Compare commits

..

1 Commits

Author SHA1 Message Date
Dimitry Kolyshev
261cf35e52 all: upd dnsproxy 2024-05-24 13:12:24 +03:00
308 changed files with 31930 additions and 31423 deletions

View File

@@ -1,7 +1,7 @@
'name': 'build' 'name': 'build'
'env': 'env':
'GO_VERSION': '1.22.4' 'GO_VERSION': '1.22.3'
'NODE_VERSION': '16' 'NODE_VERSION': '16'
'on': 'on':

View File

@@ -1,7 +1,7 @@
'name': 'lint' 'name': 'lint'
'env': 'env':
'GO_VERSION': '1.22.4' 'GO_VERSION': '1.22.3'
'on': 'on':
'push': 'push':

View File

@@ -23,26 +23,6 @@ See also the [v0.107.51 GitHub milestone][ms-v0.107.51].
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Security
- Go version has been updated to prevent the possibility of exploiting the Go
vulnerabilities fixed in [Go 1.22.4][go-1.22.4].
### Changed
- Frontend rewritten in TypeScript.
- The HTTP server's write timeout has been increased from 1 minute to 5 minutes
to match the one used by AdGuard Home's HTTP client to fetch filtering-list
data ([#7041]).
### Deprecated
- Node 18 support, Node 20 will be required in future releases.
[#7041]: https://github.com/AdguardTeam/AdGuardHome/issues/7041
[go-1.22.4]: https://groups.google.com/g/golang-announce/c/XbxouI9gY7k/
<!-- <!--
NOTE: Add new changes ABOVE THIS COMMENT. NOTE: Add new changes ABOVE THIS COMMENT.
--> -->

View File

@@ -27,7 +27,7 @@ DIST_DIR = dist
GOAMD64 = v1 GOAMD64 = v1
GOPROXY = https://goproxy.cn|https://proxy.golang.org|direct GOPROXY = https://goproxy.cn|https://proxy.golang.org|direct
GOSUMDB = sum.golang.google.cn GOSUMDB = sum.golang.google.cn
GOTOOLCHAIN = go1.22.4 GOTOOLCHAIN = go1.22.3
GPG_KEY = devteam@adguard.com GPG_KEY = devteam@adguard.com
GPG_KEY_PASSPHRASE = not-a-real-password GPG_KEY_PASSPHRASE = not-a-real-password
NPM = npm NPM = npm

View File

@@ -206,8 +206,9 @@ Run `make init` to prepare the development environment.
You will need this to build AdGuard Home: You will need this to build AdGuard Home:
- [Go](https://golang.org/dl/) v1.22 or later; - [Go](https://golang.org/dl/) v1.22 or later;
- [Node.js](https://nodejs.org/en/download/) v18.18 or later; - [Node.js](https://nodejs.org/en/download/) v16 or later;
- [npm](https://www.npmjs.com/) v8 or later; - [npm](https://www.npmjs.com/) v8 or later;
- [yarn](https://yarnpkg.com/) v1.22.5 or later.
### <a href="#building" id="building" name="building">Building</a> ### <a href="#building" id="building" name="building">Building</a>
@@ -219,6 +220,14 @@ cd AdGuardHome
make make
``` ```
#### <a href="#building-node" id="building-node" name="building-node">Building with Node.js 17 and later</a>
In order to build AdGuard Home with Node.js 17 and later, specify `--openssl-legacy-provider` option.
```sh
export NODE_OPTIONS=--openssl-legacy-provider
```
> [!WARNING] > [!WARNING]
> The non-standard `-j` flag is currently not supported, so building with `make -j 4` or setting your `MAKEFLAGS` to include, for example, `-j 4` is likely to break the build. If you do have your `MAKEFLAGS` set to that, and you don't want to change it, you can override it by running `make -j 1`. > The non-standard `-j` flag is currently not supported, so building with `make -j 4` or setting your `MAKEFLAGS` to include, for example, `-j 4` is likely to break the build. If you do have your `MAKEFLAGS` set to that, and you don't want to change it, you can override it by running `make -j 1`.

View File

@@ -7,8 +7,8 @@
# Make sure to sync any changes with the branch overrides below. # Make sure to sync any changes with the branch overrides below.
'variables': 'variables':
'channel': 'edge' 'channel': 'edge'
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:1.1'
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1' 'dockerGo': 'adguard/go-builder:1.22.3--1'
'stages': 'stages':
- 'Build frontend': - 'Build frontend':
@@ -265,8 +265,8 @@
# need to build a few of these. # need to build a few of these.
'variables': 'variables':
'channel': 'beta' 'channel': 'beta'
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:1.1'
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1' 'dockerGo': 'adguard/go-builder:1.22.3--1'
# release-vX.Y.Z branches are the branches from which the actual final # release-vX.Y.Z branches are the branches from which the actual final
# release is built. # release is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+': - '^release-v[0-9]+\.[0-9]+\.[0-9]+':
@@ -281,5 +281,5 @@
# are the ones that actually get released. # are the ones that actually get released.
'variables': 'variables':
'channel': 'release' 'channel': 'release'
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:1.1'
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1' 'dockerGo': 'adguard/go-builder:1.22.3--1'

View File

@@ -5,8 +5,8 @@
'key': 'AHBRTSPECS' 'key': 'AHBRTSPECS'
'name': 'AdGuard Home - Build and run tests' 'name': 'AdGuard Home - Build and run tests'
'variables': 'variables':
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:1.1'
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1' 'dockerGo': 'adguard/go-builder:1.22.3--1'
'channel': 'development' 'channel': 'development'
'stages': 'stages':
@@ -194,6 +194,6 @@
# Set the default release channel on the release branch to beta, as we # Set the default release channel on the release branch to beta, as we
# may need to build a few of these. # may need to build a few of these.
'variables': 'variables':
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:1.1'
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1' 'dockerGo': 'adguard/go-builder:1.22.3--1'
'channel': 'candidate' 'channel': 'candidate'

60
client/.eslintrc.json vendored
View File

@@ -1,13 +1,9 @@
{ {
"plugins": ["prettier"], "parser": "babel-eslint",
"extends": [ "extends": [
"airbnb-base",
"prettier",
"eslint:recommended",
"plugin:react/recommended", "plugin:react/recommended",
"plugin:@typescript-eslint/recommended" "airbnb-base"
], ],
"parser": "@typescript-eslint/parser",
"env": { "env": {
"jest": true, "jest": true,
"node": true, "node": true,
@@ -20,21 +16,50 @@
"version": "16.4" "version": "16.4"
}, },
"import/resolver": { "import/resolver": {
"node": { "webpack": {
"extensions": [".js", ".jsx", ".ts", ".tsx"] "config": "webpack.common.js"
} }
} }
}, },
"rules": { "rules": {
"@typescript-eslint/no-explicit-any": "off", "indent": [
"import/extensions": [
"error", "error",
"ignorePackages", 4,
{ {
"js": "never", "SwitchCase": 1,
"jsx": "never", "VariableDeclarator": 1,
"ts": "never", "outerIIFEBody": 1,
"tsx": "never" "FunctionDeclaration": {
"parameters": 1,
"body": 1
},
"FunctionExpression": {
"parameters": 1,
"body": 1
},
"CallExpression": {
"arguments": 1
},
"ArrayExpression": 1,
"ObjectExpression": 1,
"ImportDeclaration": 1,
"flatTernaryExpressions": false,
"ignoredNodes": [
"JSXElement",
"JSXElement > *",
"JSXAttribute",
"JSXIdentifier",
"JSXNamespacedName",
"JSXMemberExpression",
"JSXSpreadAttribute",
"JSXExpressionContainer",
"JSXOpeningElement",
"JSXClosingElement",
"JSXText",
"JSXEmptyExpression",
"JSXSpreadChild"
],
"ignoreComments": false
} }
], ],
"class-methods-use-this": "off", "class-methods-use-this": "off",
@@ -43,7 +68,10 @@
"no-console": [ "no-console": [
"warn", "warn",
{ {
"allow": ["warn", "error"] "allow": [
"warn",
"error"
]
} }
], ],
"import/no-extraneous-dependencies": [ "import/no-extraneous-dependencies": [

View File

@@ -1 +1 @@
*.ts text eol=lf *.js text eol=lf

10
client/.prettierrc vendored
View File

@@ -1,10 +0,0 @@
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": true,
"tabWidth": 4,
"semi": true,
"arrowParens": "always",
}

46
client/.stylelintrc vendored Normal file
View File

@@ -0,0 +1,46 @@
{
"defaultSeverity": "warning",
"rules": {
"block-closing-brace-empty-line-before": "never",
"block-no-empty": true,
"block-opening-brace-newline-after": "always",
"block-opening-brace-space-before": "always",
"color-hex-case": "lower",
"color-named": "never",
"color-no-invalid-hex": true,
"length-zero-no-unit": true,
"declaration-block-trailing-semicolon": "always",
"custom-property-empty-line-before": ["always", {
"except": [
"after-custom-property",
"first-nested"
]
}],
"declaration-block-no-duplicate-properties": true,
"declaration-colon-space-after": "always",
"declaration-empty-line-before": ["always", {
"except": [
"after-declaration",
"first-nested",
"after-comment"
]
}],
"font-weight-notation": "numeric",
"indentation": [4, {
"except": ["value"]
}],
"max-empty-lines": 2,
"no-missing-end-of-source-newline": true,
"number-leading-zero": "always",
"property-no-unknown": [true, {
"ignoreProperties": "/lost-.+/"
}],
"rule-empty-line-before": [ "always-multi-line", {
"except": ["first-nested"],
"ignore": ["after-comment"]
}],
"string-quotes": "double",
"value-list-comma-space-after": "always",
"unit-case": "lower"
}
}

View File

@@ -1,44 +0,0 @@
module.exports = {
rules: {
"selector-type-no-unknown": true,
"block-closing-brace-empty-line-before": "never",
"block-no-empty": true,
"block-opening-brace-newline-after": "always",
"block-opening-brace-space-before": "always",
"color-hex-case": "lower",
"color-named": "never",
"color-no-invalid-hex": true,
"length-zero-no-unit": true,
"declaration-block-trailing-semicolon": "always",
"custom-property-empty-line-before": ["always", {
"except": [
"after-custom-property",
"first-nested"
]
}],
"declaration-block-no-duplicate-properties": true,
"declaration-colon-space-after": "always",
"declaration-empty-line-before": ["always", {
"except": [
"after-declaration",
"first-nested",
"after-comment"
]
}],
"font-weight-notation": "numeric",
"indentation": [4, {
"except": ["value"]
}],
"max-empty-lines": 2,
"no-missing-end-of-source-newline": true,
"number-leading-zero": "always",
"property-no-unknown": true,
"rule-empty-line-before": ["always-multi-line", {
"except": ["first-nested"],
"ignore": ["after-comment"]
}],
"string-quotes": "double",
"value-list-comma-space-after": "always",
"unit-case": "lower"
}
}

View File

@@ -1,14 +0,0 @@
module.exports = (api) => {
api.cache(false);
return {
presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-class-properties',
'@babel/plugin-transform-object-rest-spread',
'@babel/plugin-transform-nullish-coalescing-operator',
'@babel/plugin-transform-optional-chaining',
'react-hot-loader/babel',
],
};
};

17
client/babel.config.js vendored Normal file
View File

@@ -0,0 +1,17 @@
module.exports = (api) => {
api.cache(false);
return {
presets: [
'@babel/preset-env',
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-optional-chaining',
'react-hot-loader/babel',
],
};
};

9
client/constants.js vendored
View File

@@ -1,6 +1,11 @@
export const BUILD_ENVS = { const BUILD_ENVS = {
dev: 'development', dev: 'development',
prod: 'production', prod: 'production',
}; };
export const BASE_URL = 'control'; const BASE_URL = 'control';
module.exports = {
BUILD_ENVS,
BASE_URL,
};

6
client/global.d.ts vendored
View File

@@ -1,6 +0,0 @@
import React from 'react';
declare module '*.svg' {
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
export default content;
}

5
client/jest.config.js vendored Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
transform: {
'^.+\\.jsx?$': 'babel-jest',
},
};

View File

@@ -1,6 +0,0 @@
export default {
testEnvironment: 'jsdom',
transform: {
'^.+\\.tsx?$': 'babel-jest',
},
};

38385
client/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

110
client/package.json vendored
View File

@@ -3,23 +3,19 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build-dev": "cross-env NODE_ENV=development BUILD_ENV=dev webpack --config webpack.dev.js", "build-dev": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js",
"build-prod": "cross-env BUILD_ENV=prod webpack --config webpack.prod.js", "build-prod": "cross-env BUILD_ENV=prod webpack --config webpack.prod.js",
"watch": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch", "watch": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch",
"watch:hot": "cross-env BUILD_ENV=dev webpack-dev-server --config webpack.dev.js", "watch:hot": "cross-env BUILD_ENV=dev webpack-dev-server --config webpack.dev.js",
"lint": "echo 'Lint temporarily disabled'", "lint": "eslint src",
"lint-new": "eslint './src/**/*.(ts|tsx)'", "lint:fix": "eslint src --fix",
"lint:fix": "eslint './src/**/*.(ts|tsx)' --fix",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch"
"typecheck": "tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch"
}, },
"type": "module",
"dependencies": { "dependencies": {
"@nivo/line": "^0.64.0", "@nivo/line": "^0.64.0",
"axios": "^0.19.2", "axios": "^0.19.2",
"classnames": "^2.5.1", "classnames": "^2.2.6",
"countries-and-timezones": "^3.6.0", "countries-and-timezones": "^3.6.0",
"date-fns": "^1.29.0", "date-fns": "^1.29.0",
"i18next": "^19.6.2", "i18next": "^19.6.2",
@@ -28,8 +24,7 @@
"js-yaml": "^3.14.0", "js-yaml": "^3.14.0",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"nanoid": "^3.1.9", "nanoid": "^3.1.9",
"popper.js": "^1.16.1", "prop-types": "^15.7.2",
"prop-types": "^15.8.1",
"query-string": "^6.13.1", "query-string": "^6.13.1",
"react": "^16.13.1", "react": "^16.13.1",
"react-click-outside": "^3.0.1", "react-click-outside": "^3.0.1",
@@ -43,64 +38,53 @@
"react-router-hash-link": "^1.2.2", "react-router-hash-link": "^1.2.2",
"react-select": "^3.1.0", "react-select": "^3.1.0",
"react-table": "^6.11.4", "react-table": "^6.11.4",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.1",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-actions": "^2.6.5", "redux-actions": "^2.6.5",
"redux-form": "^8.3.10", "redux-form": "^8.3.5",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"ts-migrate": "^0.1.35", "url-polyfill": "^1.1.9"
"url-polyfill": "^1.1.12"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.5", "@babel/core": "^7.9.6",
"@babel/plugin-transform-class-properties": "^7.24.1", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-transform-object-rest-spread": "^7.24.5", "@babel/plugin-proposal-object-rest-spread": "^7.9.6",
"@babel/plugin-transform-optional-chaining": "^7.24.5", "@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.24.3", "@babel/plugin-transform-runtime": "^7.9.6",
"@babel/preset-env": "^7.24.5", "@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.24.1", "@babel/preset-react": "^7.9.4",
"@types/jest": "^29.5.12", "autoprefixer": "^9.8.0",
"@types/lodash": "^4.17.4", "babel-eslint": "^10.1.0",
"@types/react": "^17.0.80", "babel-loader": "^8.1.0",
"@types/react-dom": "^18.3.0", "clean-webpack-plugin": "^3.0.0",
"@types/react-redux": "^7.1.33", "copy-webpack-plugin": "^6.0.1",
"@types/react-router-dom": "^5.3.3", "cross-env": "^7.0.2",
"@types/react-table": "^7.7.20", "css-loader": "^3.5.3",
"@types/redux-actions": "^2.6.5", "eslint": "^6.8.0",
"@types/redux-form": "^8.3.10", "eslint-config-airbnb": "^18.1.0",
"@typescript-eslint/eslint-plugin": "^7.11.0", "eslint-import-resolver-webpack": "^0.12.1",
"@typescript-eslint/parser": "^7.10.0", "eslint-loader": "^4.0.2",
"babel-loader": "^9.1.3", "eslint-plugin-import": "^2.22.1",
"clean-webpack-plugin": "^4.0.0", "eslint-plugin-jsx-a11y": "^6.2.3",
"copy-webpack-plugin": "^12.0.2", "eslint-plugin-react": "^7.24.0",
"cross-env": "^7.0.3", "eslint-plugin-react-hooks": "^2.5.0",
"css-loader": "^7.1.2", "file-loader": "6.0.0",
"eslint": "^8.57.0", "html-webpack-plugin": "^4.3.0",
"eslint-config-airbnb": "^19.0.4", "jest": "^26.0.1",
"eslint-config-prettier": "^9.1.0", "mini-css-extract-plugin": "^0.9.0",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.2",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jscodeshift": "^0.15.2",
"mini-css-extract-plugin": "^2.9.0",
"path": "^0.12.7", "path": "^0.12.7",
"postcss-loader": "^8.1.1", "postcss-flexbugs-fixes": "4.2.1",
"prettier": "^3.2.5", "postcss-loader": "^3.0.0",
"react-hot-loader": "^4.13.1", "react-hot-loader": "^4.12.21",
"style-loader": "^4.0.0", "style-loader": "^1.2.1",
"stylelint": "^16.5.0", "stylelint": "^13.5.0",
"ts-loader": "^9.5.1", "stylelint-webpack-plugin": "2.0.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.0",
"webpack": "^5.91.0", "webpack": "^4.43.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^3.3.11",
"webpack-dev-server": "^5.0.4", "webpack-dev-server": "^3.11.0",
"webpack-merge": "^5.10.0" "webpack-merge": "^4.2.2"
}, },
"browserslist": { "browserslist": {
"development": [ "development": [

View File

@@ -13,14 +13,14 @@
"fallback_dns_desc": "Listaus DNS-varapalvelimista, joita käytetään kun lähtevät DNS-palvelimet eivät vastaa. Syntaksi on sama kuin yllä olevassa pääylävirrat-kentässä.", "fallback_dns_desc": "Listaus DNS-varapalvelimista, joita käytetään kun lähtevät DNS-palvelimet eivät vastaa. Syntaksi on sama kuin yllä olevassa pääylävirrat-kentässä.",
"fallback_dns_placeholder": "Syötä yksi DNS-varapalvelin per rivi", "fallback_dns_placeholder": "Syötä yksi DNS-varapalvelin per rivi",
"local_ptr_title": "Yksityiset käänteis-DNS-palvelimet", "local_ptr_title": "Yksityiset käänteis-DNS-palvelimet",
"local_ptr_desc": "AdGuard Homen yksityisille PTR-, SOA- ja NS-pyynnöille käyttämät DNS-palvelimet. Pyyntöä luokitellaan yksityiseksi, jos se pyytää yksityistä IP-aluetta (kuten \"192.168.12.34\") käyttävän aliverkon sisältävää ARPA-verkkotunnusta ja on lähtöisin päätteeltä, jolla on yksityinen IP-osoite. Jos tätä ei ole määritetty, käytetään käyttöjärjestelmän oletusarvoisia DNS-resolvereita (AdGuard Homen IP-osoitteet pois lukien).", "local_ptr_desc": "DNS-palvelimet, joita AdGuard Home käyttää paikallisille PTR-pyynnöille. Näitä palvelimia käytetään yksityistä IP-osoitetta käyttävien PTR-pyyntöjen osoitteiden, kuten \"192.168.12.34\", selvitykseen käänteis-DNS:n avulla. Jos ei käytössä, AdGuard Home käyttää käyttöjärjestelmän oletusarvoisia DNS-resolvereita, poislukien AdGuard Homen omat osoitteet.",
"local_ptr_default_resolver": "Oletusarvoisesti AdGuard Home käyttää seuraavia käänteis-DNS-resolvereita: {{ip}}.", "local_ptr_default_resolver": "Oletusarvoisesti AdGuard Home käyttää seuraavia käänteis-DNS-resolvereita: {{ip}}.",
"local_ptr_no_default_resolver": "AdGuard Home ei voinut määrittää tälle järjestelmälle sopivaa yksityistä käänteis-DNS-resolveria.", "local_ptr_no_default_resolver": "AdGuard Home ei voinut määrittää tälle järjestelmälle sopivaa yksityistä käänteis-DNS-resolveria.",
"local_ptr_placeholder": "Syötä yksi IP-osoite per rivi", "local_ptr_placeholder": "Syötä yksi IP-osoite per rivi",
"resolve_clients_title": "Käytä päätelaitteiden IP-osoitteille käänteistä selvitystä", "resolve_clients_title": "Käytä päätelaitteiden IP-osoitteille käänteistä selvitystä",
"resolve_clients_desc": "Selvitä päätelaitteiden IP-osoitteiden isäntänimet käänteisesti lähettämällä PTR-pyynnöt sopiville resolvereille (yksityiset DNS-palvelimet paikallisille päätelaitteille, ylävirtapalvelimet päätelaitteille, joilla on julkiset IP-osoitteet).", "resolve_clients_desc": "Selvitä päätelaitteiden IP-osoitteiden isäntänimet käänteisesti lähettämällä PTR-pyynnöt sopiville resolvereille (yksityiset DNS-palvelimet paikallisille päätelaitteille, ylävirtapalvelimet päätelaitteille, joilla on julkiset IP-osoitteet).",
"use_private_ptr_resolvers_title": "Käytä yksityisiä käänteis-DNS-resolvereita", "use_private_ptr_resolvers_title": "Käytä yksityisiä käänteis-DNS-resolvereita",
"use_private_ptr_resolvers_desc": "Selvitä yksityisiä IP-osoitteita sisältävien ARPA-verkkotunnusten PTR-, SOA- ja NS-pyynnöt käyttäen yksityisiä ylävirtapalvelimia, DHCP:tä, /etc/hosts-määrityksiä, yms. Jos tämä ei ole käytössä, AdGuard Home vastaa tällaisiin pyyntöihin NXDOMAIN-tiedolla.", "use_private_ptr_resolvers_desc": "Suorita käänteis-DNS-selvitykset paikallisesti tarjotuille osoitteille käyttäen näitä ylävirtapalvelimia. Jos ei käytössä, vastaa AdGuard Home kaikkiin sen tyyppisiin PTR-pyyntöihin NXDOMAIN-arvolla, pois lukien DHCP, /etc/hosts, yms. -tiedoista tunnistettut päätelaitteet.",
"check_dhcp_servers": "Etsi DHCP-palvelimia", "check_dhcp_servers": "Etsi DHCP-palvelimia",
"save_config": "Tallenna asetukset", "save_config": "Tallenna asetukset",
"enabled_dhcp": "DHCP-palvelin otettiin käyttöön", "enabled_dhcp": "DHCP-palvelin otettiin käyttöön",
@@ -326,7 +326,7 @@
"blocking_ipv6_desc": "Estettyyn AAAA-pyyntöön palautettava IP-osoite", "blocking_ipv6_desc": "Estettyyn AAAA-pyyntöön palautettava IP-osoite",
"blocking_mode_default": "Oletus: Vastaa IP-nollaosoitteella (0.0.0.0 korvaa A; :: korvaa AAAA) kun estetään mainoseston säännöllä; vastaa säännön määrittämällä IP-osoitteella kun estetään /etc/hosts-tyyppisellä säännöllä", "blocking_mode_default": "Oletus: Vastaa IP-nollaosoitteella (0.0.0.0 korvaa A; :: korvaa AAAA) kun estetään mainoseston säännöllä; vastaa säännön määrittämällä IP-osoitteella kun estetään /etc/hosts-tyyppisellä säännöllä",
"blocking_mode_refused": "REFUSED: Vastaa REFUSED-koodilla", "blocking_mode_refused": "REFUSED: Vastaa REFUSED-koodilla",
"blocking_mode_nxdomain": "NXDOMAIN: Vastaa NXDOMAIN-tiedolla", "blocking_mode_nxdomain": "NXDOMAIN: Vastaa NXDOMAIN-koodilla",
"blocking_mode_null_ip": "Tyhjä IP: Vastaa IP-nollaosoitteella (0.0.0.0 korvaa A; :: korvaa AAAA)", "blocking_mode_null_ip": "Tyhjä IP: Vastaa IP-nollaosoitteella (0.0.0.0 korvaa A; :: korvaa AAAA)",
"blocking_mode_custom_ip": "Mukautettu IP: Vastaa manuaalisesti määritetyllä IP-osoitteella", "blocking_mode_custom_ip": "Mukautettu IP: Vastaa manuaalisesti määritetyllä IP-osoitteella",
"theme_auto": "Automaattinen", "theme_auto": "Automaattinen",
@@ -501,8 +501,8 @@
"setup_dns_privacy_ios_1": "<0>DNSCloak</0> tukee <1>DNS-over-HTTPS</1>, mutta oman palvelimen käyttö' varten sille on luotava <2>DNS Stamp</2> -merkintä.", "setup_dns_privacy_ios_1": "<0>DNSCloak</0> tukee <1>DNS-over-HTTPS</1>, mutta oman palvelimen käyttö' varten sille on luotava <2>DNS Stamp</2> -merkintä.",
"setup_dns_privacy_ios_2": "<0>AdGuard iOS:lle</0> tukee <1>DNS-over-HTTPS</1> ja <1>DNS-over-TLS</1> -toteutuksia.", "setup_dns_privacy_ios_2": "<0>AdGuard iOS:lle</0> tukee <1>DNS-over-HTTPS</1> ja <1>DNS-over-TLS</1> -toteutuksia.",
"setup_dns_privacy_other_title": "Muita toteutuksia", "setup_dns_privacy_other_title": "Muita toteutuksia",
"setup_dns_privacy_other_1": "AdGuard Home voi itse olla suojattu DNS -pääte millä tahansa alustalla.", "setup_dns_privacy_other_1": "AdGuard Home voi itse olla turvallinen DNS-päätelaite millä tahansa alustalla.",
"setup_dns_privacy_other_2": "<0>dnsproxy</0> tukee kaikkia tunnettuja suojattuja DNS-protokollia.", "setup_dns_privacy_other_2": "<0>dnsproxy</0> tukee kaikkia tunnettuja turvallisia DNS-protokollia.",
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> tukee <1>DNS-over-HTTPS</1> -protokollaa.", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> tukee <1>DNS-over-HTTPS</1> -protokollaa.",
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> tukee <1>DNS-over-HTTPS</1>-toteutusta.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> tukee <1>DNS-over-HTTPS</1>-toteutusta.",
"setup_dns_privacy_other_5": "Löydät lisää toteutuksia <0>täältä</0> ja <1>täältä</1>.", "setup_dns_privacy_other_5": "Löydät lisää toteutuksia <0>täältä</0> ja <1>täältä</1>.",

View File

@@ -13,14 +13,14 @@
"fallback_dns_desc": "アップストリームDNSサーバーが応答しない場合に使用されるフォールバックDNSサーバーのリストです。構文は上記のmain upstreamsフィールドと同じです。", "fallback_dns_desc": "アップストリームDNSサーバーが応答しない場合に使用されるフォールバックDNSサーバーのリストです。構文は上記のmain upstreamsフィールドと同じです。",
"fallback_dns_placeholder": "フォールバックDNSサーバーを1行に1つずつ入力してください。", "fallback_dns_placeholder": "フォールバックDNSサーバーを1行に1つずつ入力してください。",
"local_ptr_title": "プライベートリバースDNSサーバー", "local_ptr_title": "プライベートリバースDNSサーバー",
"local_ptr_desc": "AdGuard Home がプライベート PTR、SOA、および NS リクエストに使用する DNS サーバープライベート IP 範囲内のサブネット (「192.168.12.34」など) を含む ARPA ドメインを要求し、プライベート IP アドレスを持つクライアントから来たリクエストが、プライベートリクエストとみなされます。設定が特に指定されていない場合、OS のデフォルト DNS リゾルバAdGuard Home の IP アドレスを除く)が使用されます。", "local_ptr_desc": "AdGuard HomeがローカルPTRクエリに使用するDNSサーバーです。これらのサーバーは、rDNSを使ってプライベートIPアドレス(例えば\"192.168.12.34\")を持つクライアントのホスト名を解決するために使用されます。設定されていない場合、AdGuard HomeはOSのデフォルトDNSリゾルバーのアドレスAdGuard Home自体のアドレスを除く)を自動的に使用します。",
"local_ptr_default_resolver": "デフォルトでは、AdGuard Homeは次のリバースDNSリゾルバを使用します: {{ip}}", "local_ptr_default_resolver": "デフォルトでは、AdGuard Homeは次のリバースDNSリゾルバを使用します: {{ip}}",
"local_ptr_no_default_resolver": "AdGuard Homeは、このシステムに適したプライベートリバースDNSリゾルバを特定できませんでした。", "local_ptr_no_default_resolver": "AdGuard Homeは、このシステムに適したプライベートリバースDNSリゾルバを特定できませんでした。",
"local_ptr_placeholder": "IPアドレスを1行に1つずづ入力してください。", "local_ptr_placeholder": "IPアドレスを1行に1つずづ入力してください。",
"resolve_clients_title": "クライアントのIPアドレスの逆解決を有効にする", "resolve_clients_title": "クライアントのIPアドレスの逆解決を有効にする",
"resolve_clients_desc": "対応するリゾルバーローカルクライアントの場合はプライベートDNSサーバ、パブリックIPを持つクライアントの場合はアップストリームサーバーにPTRクエリを送信することにより、クライアントのIPアドレスをホストネームに逆解決します。", "resolve_clients_desc": "対応するリゾルバーローカルクライアントの場合はプライベートDNSサーバ、パブリックIPを持つクライアントの場合はアップストリームサーバーにPTRクエリを送信することにより、クライアントのIPアドレスをホストネームに逆解決します。",
"use_private_ptr_resolvers_title": "プライベートリバースDNSリゾルバを使用", "use_private_ptr_resolvers_title": "プライベートリバースDNSリゾルバを使用",
"use_private_ptr_resolvers_desc": "プライベートアップストリームサーバー、DHCP、/etc/hosts などを通じて、プライベート IP アドレスを含む ARPA ドメインの PTR、SOA、および NS リクエストを解決します。無効にした場合、AdGuard Home はこのようなリクエストのすべてに NXDOMAIN で応答します。", "use_private_ptr_resolvers_desc": "これらのアップストリームサーバーを使用して、ローカルで提供されるアドレスのリバースDNSルックアップを実行します。無効にすると、AdGuard Homeは、DHCP, /etc/hosts などから認識されるクライアントを除き、すべてのこのようなPTR要求にNXDOMAINで応答します。",
"check_dhcp_servers": "DHCPサーバをチェックする", "check_dhcp_servers": "DHCPサーバをチェックする",
"save_config": "構成を保存する", "save_config": "構成を保存する",
"enabled_dhcp": "DHCPサーバを有効にしました", "enabled_dhcp": "DHCPサーバを有効にしました",

View File

@@ -9,7 +9,7 @@
"load_balancing_desc": "Fråga en uppströmsserver åt gången. AdGuard Home använder sin viktade slumpmässiga algoritm för att välja server så att den snabbaste servern används oftare.", "load_balancing_desc": "Fråga en uppströmsserver åt gången. AdGuard Home använder sin viktade slumpmässiga algoritm för att välja server så att den snabbaste servern används oftare.",
"bootstrap_dns": "Bootstrap-DNS-servrar", "bootstrap_dns": "Bootstrap-DNS-servrar",
"bootstrap_dns_desc": "IP-adresser för DNS-servrar som används för att lösa IP-adresser för de DoH/DoT-resolvers som du anger som uppströms. Kommentarer är inte tillåtna.", "bootstrap_dns_desc": "IP-adresser för DNS-servrar som används för att lösa IP-adresser för de DoH/DoT-resolvers som du anger som uppströms. Kommentarer är inte tillåtna.",
"fallback_dns_title": "Reserv DNS-servrar", "fallback_dns_title": "Reserv-DNS-servrar",
"fallback_dns_desc": "Lista över reserv-DNS-servrar som används när uppströms DNS-servrar inte svarar. Syntaxen är densamma som i huvuduppströmsfältet ovan.", "fallback_dns_desc": "Lista över reserv-DNS-servrar som används när uppströms DNS-servrar inte svarar. Syntaxen är densamma som i huvuduppströmsfältet ovan.",
"fallback_dns_placeholder": "Ange en reserv-DNS-server per rad", "fallback_dns_placeholder": "Ange en reserv-DNS-server per rad",
"local_ptr_title": "Privata omvända DNS-servrar", "local_ptr_title": "Privata omvända DNS-servrar",
@@ -141,8 +141,8 @@
"number_of_dns_query_blocked_24_hours": "Antalet DNS-förfrågningar som blockerades av annonsfilter och värdens blockeringsklistor", "number_of_dns_query_blocked_24_hours": "Antalet DNS-förfrågningar som blockerades av annonsfilter och värdens blockeringsklistor",
"number_of_dns_query_blocked_24_hours_by_sec": "Antalet DNS-förfrågningar som blockerades av AdGuards modul för surfsäkerhet", "number_of_dns_query_blocked_24_hours_by_sec": "Antalet DNS-förfrågningar som blockerades av AdGuards modul för surfsäkerhet",
"number_of_dns_query_blocked_24_hours_adult": "Antalet vuxensajter som blockerats", "number_of_dns_query_blocked_24_hours_adult": "Antalet vuxensajter som blockerats",
"enforced_save_search": "Genomdrev SafeSearch", "enforced_save_search": "Aktivering av Säker surf",
"number_of_dns_query_to_safe_search": "Antalet DNS-förfrågningar till sökmotorer för vilka SafeSearch genomdrevs", "number_of_dns_query_to_safe_search": "Antalet DNS-förfrågningar mot sökmotorer där Säker surf tvingats",
"average_processing_time": "Genomsnittlig processtid", "average_processing_time": "Genomsnittlig processtid",
"average_upstream_response_time": "Genomsnittlig svarstid uppströmsserver", "average_upstream_response_time": "Genomsnittlig svarstid uppströmsserver",
"response_time": "Svarstid", "response_time": "Svarstid",
@@ -153,7 +153,7 @@
"use_adguard_browsing_sec_hint": "AdGuard Home kommer att kontrollera om en domän är blockerad av webbservicen surfsäkerhet. Med en integritetsvänlig metod görs en API-lookup för att kontrollera: endast ett kort prefix i domännamnet SHA256 hash skickas till servern.", "use_adguard_browsing_sec_hint": "AdGuard Home kommer att kontrollera om en domän är blockerad av webbservicen surfsäkerhet. Med en integritetsvänlig metod görs en API-lookup för att kontrollera: endast ett kort prefix i domännamnet SHA256 hash skickas till servern.",
"use_adguard_parental": "Använda AdGuards webbservice för föräldrakontroll", "use_adguard_parental": "Använda AdGuards webbservice för föräldrakontroll",
"use_adguard_parental_hint": "AdGuard Home kommer att kontrollera domäner för innehåll av vuxenmaterial . Samma integritetsvänliga metod för API-lookup som tillämpas i webbservicens surfsäkerhet används.", "use_adguard_parental_hint": "AdGuard Home kommer att kontrollera domäner för innehåll av vuxenmaterial . Samma integritetsvänliga metod för API-lookup som tillämpas i webbservicens surfsäkerhet används.",
"enforce_safe_search": "Använd SafeSearch", "enforce_safe_search": "Använd säker webbsökning",
"enforce_save_search_hint": "AdGuard Home kommer tvinga säker surf på följande sökmotorer: Google, Youtube, Bing, DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "AdGuard Home kommer tvinga säker surf på följande sökmotorer: Google, Youtube, Bing, DuckDuckGo, Yandex, Pixabay.",
"no_servers_specified": "Inga servrar angivna", "no_servers_specified": "Inga servrar angivna",
"general_settings": "Allmänna inställningar", "general_settings": "Allmänna inställningar",
@@ -657,7 +657,7 @@
"cache_optimistic": "Optimistisk cachning", "cache_optimistic": "Optimistisk cachning",
"cache_optimistic_desc": "Få AdGuard Home att svara från cachen även när posterna har gått ut och försök även uppdatera dem.", "cache_optimistic_desc": "Få AdGuard Home att svara från cachen även när posterna har gått ut och försök även uppdatera dem.",
"filter_category_general": "Allmänt", "filter_category_general": "Allmänt",
"filter_category_security": "Säkerhet", "filter_category_security": "säkerhet",
"filter_category_regional": "Regional", "filter_category_regional": "Regional",
"filter_category_other": "Övrigt", "filter_category_other": "Övrigt",
"filter_category_general_desc": "Listor som blockerar spårning och reklam på de flesta enheterna", "filter_category_general_desc": "Listor som blockerar spårning och reklam på de flesta enheterna",

View File

@@ -1,15 +1,26 @@
import { sortIp, countClientsStatistics, findAddressType, subnetMaskToBitMask } from '../helpers/helpers'; import {
sortIp,
countClientsStatistics,
findAddressType,
subnetMaskToBitMask,
} from '../helpers/helpers';
import { ADDRESS_TYPES } from '../helpers/constants'; import { ADDRESS_TYPES } from '../helpers/constants';
describe('sortIp', () => { describe('sortIp', () => {
describe('ipv4', () => { describe('ipv4', () => {
test('one octet differ', () => { test('one octet differ', () => {
const arr = ['127.0.2.0', '127.0.3.0', '127.0.1.0']; const arr = [
const sortedArr = ['127.0.1.0', '127.0.2.0', '127.0.3.0']; '127.0.2.0',
'127.0.3.0',
'127.0.1.0',
];
const sortedArr = [
'127.0.1.0',
'127.0.2.0',
'127.0.3.0',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
test('few octets differ', () => { test('few octets differ', () => {
const arr = [ const arr = [
'192.168.11.10', '192.168.11.10',
@@ -47,7 +58,6 @@ describe('sortIp', () => {
'192.168.11.10', '192.168.11.10',
'192.168.11.11', '192.168.11.11',
]; ];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
// Example from issue https://github.com/AdguardTeam/AdGuardHome/issues/1778#issuecomment-640937599 // Example from issue https://github.com/AdguardTeam/AdGuardHome/issues/1778#issuecomment-640937599
@@ -73,26 +83,36 @@ describe('sortIp', () => {
'192.168.2.200', '192.168.2.200',
'192.168.3.1', '192.168.3.1',
]; ];
expect(arr2.sort(sortIp)).toStrictEqual(sortedArr2); expect(arr2.sort(sortIp)).toStrictEqual(sortedArr2);
}); });
}); });
describe('ipv6', () => { describe('ipv6', () => {
test('only long form', () => { test('only long form', () => {
const arr = ['2001:db8:11a3:9d7:0:0:0:2', '2001:db8:11a3:9d7:0:0:0:3', '2001:db8:11a3:9d7:0:0:0:1']; const arr = [
const sortedArr = ['2001:db8:11a3:9d7:0:0:0:1', '2001:db8:11a3:9d7:0:0:0:2', '2001:db8:11a3:9d7:0:0:0:3']; '2001:db8:11a3:9d7:0:0:0:2',
'2001:db8:11a3:9d7:0:0:0:3',
'2001:db8:11a3:9d7:0:0:0:1',
];
const sortedArr = [
'2001:db8:11a3:9d7:0:0:0:1',
'2001:db8:11a3:9d7:0:0:0:2',
'2001:db8:11a3:9d7:0:0:0:3',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
test('only short form', () => { test('only short form', () => {
const arr = ['2001:db8::', '2001:db7::', '2001:db9::']; const arr = [
const sortedArr = ['2001:db7::', '2001:db8::', '2001:db9::']; '2001:db8::',
'2001:db7::',
'2001:db9::',
];
const sortedArr = [
'2001:db7::',
'2001:db8::',
'2001:db9::',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
test('long and short forms', () => { test('long and short forms', () => {
const arr = [ const arr = [
'2001:db8::', '2001:db8::',
@@ -110,11 +130,9 @@ describe('sortIp', () => {
'2001:db7:11a3:9d7:0:0:0:2', '2001:db7:11a3:9d7:0:0:0:2',
'2001:db8::', '2001:db8::',
]; ];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
}); });
describe('ipv4 and ipv6', () => { describe('ipv4 and ipv6', () => {
test('ipv6 long form', () => { test('ipv6 long form', () => {
const arr = [ const arr = [
@@ -133,10 +151,8 @@ describe('sortIp', () => {
'2001:db8:11a3:9d7:0:0:0:2', '2001:db8:11a3:9d7:0:0:0:2',
'2001:db8:11a3:9d7:0:0:0:3', '2001:db8:11a3:9d7:0:0:0:3',
]; ];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
test('ipv6 short form', () => { test('ipv6 short form', () => {
const arr = [ const arr = [
'2001:db8:11a3:9d7::1', '2001:db8:11a3:9d7::1',
@@ -154,10 +170,8 @@ describe('sortIp', () => {
'2001:db8:11a3:9d7::2', '2001:db8:11a3:9d7::2',
'2001:db8:11a3:9d7::3', '2001:db8:11a3:9d7::3',
]; ];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
test('ipv6 long and short forms', () => { test('ipv6 long and short forms', () => {
const arr = [ const arr = [
'2001:db8:11a3:9d7::1', '2001:db8:11a3:9d7::1',
@@ -175,10 +189,8 @@ describe('sortIp', () => {
'2001:db8:11a3:9d7:0:0:0:2', '2001:db8:11a3:9d7:0:0:0:2',
'2001:db8:11a3:9d7::3', '2001:db8:11a3:9d7::3',
]; ];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
test('always put ipv4 before ipv6', () => { test('always put ipv4 before ipv6', () => {
const arr = [ const arr = [
'::1', '::1',
@@ -198,26 +210,40 @@ describe('sortIp', () => {
'2001:db8:11a3:9d7::1', '2001:db8:11a3:9d7::1',
'2001:db8:11a3:9d7:0:0:0:2', '2001:db8:11a3:9d7:0:0:0:2',
]; ];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
}); });
describe('cidr', () => { describe('cidr', () => {
test('only ipv4 cidr', () => { test('only ipv4 cidr', () => {
const arr = ['192.168.0.1/9', '192.168.0.1/7', '192.168.0.1/8']; const arr = [
const sortedArr = ['192.168.0.1/7', '192.168.0.1/8', '192.168.0.1/9']; '192.168.0.1/9',
'192.168.0.1/7',
'192.168.0.1/8',
];
const sortedArr = [
'192.168.0.1/7',
'192.168.0.1/8',
'192.168.0.1/9',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
test('ipv4 and cidr ipv4', () => { test('ipv4 and cidr ipv4', () => {
const arr = ['192.168.0.1/9', '192.168.0.1', '192.168.0.1/32', '192.168.0.1/7', '192.168.0.1/8']; const arr = [
const sortedArr = ['192.168.0.1/7', '192.168.0.1/8', '192.168.0.1/9', '192.168.0.1/32', '192.168.0.1']; '192.168.0.1/9',
'192.168.0.1',
'192.168.0.1/32',
'192.168.0.1/7',
'192.168.0.1/8',
];
const sortedArr = [
'192.168.0.1/7',
'192.168.0.1/8',
'192.168.0.1/9',
'192.168.0.1/32',
'192.168.0.1',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
test('only ipv6 cidr', () => { test('only ipv6 cidr', () => {
const arr = [ const arr = [
'2001:db8:11a3:9d7::1/32', '2001:db8:11a3:9d7::1/32',
@@ -231,10 +257,8 @@ describe('sortIp', () => {
'2001:db8:11a3:9d7::1/64', '2001:db8:11a3:9d7::1/64',
'2001:db8:11a3:9d7::1/128', '2001:db8:11a3:9d7::1/128',
]; ];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
test('ipv6 and cidr ipv6', () => { test('ipv6 and cidr ipv6', () => {
const arr = [ const arr = [
'2001:db8:11a3:9d7::1/32', '2001:db8:11a3:9d7::1/32',
@@ -250,11 +274,9 @@ describe('sortIp', () => {
'2001:db8:11a3:9d7::1/128', '2001:db8:11a3:9d7::1/128',
'2001:db8:11a3:9d7::1', '2001:db8:11a3:9d7::1',
]; ];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
}); });
describe('invalid input', () => { describe('invalid input', () => {
const originalWarn = console.warn; const originalWarn = console.warn;
@@ -269,29 +291,21 @@ describe('sortIp', () => {
test('invalid strings', () => { test('invalid strings', () => {
const arr = ['invalid ip', 'invalid cidr']; const arr = ['invalid ip', 'invalid cidr'];
expect(arr.sort(sortIp)).toStrictEqual(arr); expect(arr.sort(sortIp)).toStrictEqual(arr);
}); });
test('invalid ip', () => { test('invalid ip', () => {
const arr = ['127.0.0.2.', '.127.0.0.1.', '.2001:db8:11a3:9d7:0:0:0:0']; const arr = ['127.0.0.2.', '.127.0.0.1.', '.2001:db8:11a3:9d7:0:0:0:0'];
expect(arr.sort(sortIp)).toStrictEqual(arr); expect(arr.sort(sortIp)).toStrictEqual(arr);
}); });
test('invalid cidr', () => { test('invalid cidr', () => {
const arr = ['127.0.0.2/33', '2001:db8:11a3:9d7:0:0:0:0/129']; const arr = ['127.0.0.2/33', '2001:db8:11a3:9d7:0:0:0:0/129'];
expect(arr.sort(sortIp)).toStrictEqual(arr); expect(arr.sort(sortIp)).toStrictEqual(arr);
}); });
test('valid and invalid ip', () => { test('valid and invalid ip', () => {
const arr = ['127.0.0.4.', '127.0.0.1', '.127.0.0.3', '127.0.0.2']; const arr = ['127.0.0.4.', '127.0.0.1', '.127.0.0.3', '127.0.0.2'];
expect(arr.sort(sortIp)).toStrictEqual(arr); expect(arr.sort(sortIp)).toStrictEqual(arr);
}); });
}); });
describe('mixed', () => { describe('mixed', () => {
test('ipv4, ipv6 in short and long forms and cidr', () => { test('ipv4, ipv6 in short and long forms and cidr', () => {
const arr = [ const arr = [
@@ -340,7 +354,6 @@ describe('sortIp', () => {
'2001:db8:11a3:9d7:0:0:0:1', '2001:db8:11a3:9d7:0:0:0:1',
'2001:db8:11a3:9d7:0:0:0:2', '2001:db8:11a3:9d7:0:0:0:2',
]; ];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr); expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
}); });
}); });
@@ -350,11 +363,9 @@ describe('findAddressType', () => {
describe('ip', () => { describe('ip', () => {
expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP); expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP);
}); });
describe('cidr', () => { describe('cidr', () => {
expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR); expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR);
}); });
describe('mac', () => { describe('mac', () => {
expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN); expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN);
}); });
@@ -362,59 +373,42 @@ describe('findAddressType', () => {
describe('countClientsStatistics', () => { describe('countClientsStatistics', () => {
test('single ip', () => { test('single ip', () => {
expect( expect(countClientsStatistics(['127.0.0.1'], {
countClientsStatistics(['127.0.0.1'], { '127.0.0.1': 1,
'127.0.0.1': 1, })).toStrictEqual(1);
}),
).toStrictEqual(1);
}); });
test('multiple ip', () => { test('multiple ip', () => {
expect( expect(countClientsStatistics(['127.0.0.1', '127.0.0.2'], {
countClientsStatistics(['127.0.0.1', '127.0.0.2'], { '127.0.0.1': 1,
'127.0.0.1': 1, '127.0.0.2': 2,
'127.0.0.2': 2, })).toStrictEqual(1 + 2);
}),
).toStrictEqual(1 + 2);
}); });
test('cidr', () => { test('cidr', () => {
expect( expect(countClientsStatistics(['127.0.0.0/8'], {
countClientsStatistics(['127.0.0.0/8'], { '127.0.0.1': 1,
'127.0.0.1': 1, '127.0.0.2': 2,
'127.0.0.2': 2, })).toStrictEqual(1 + 2);
}),
).toStrictEqual(1 + 2);
}); });
test('cidr and multiple ip', () => { test('cidr and multiple ip', () => {
expect( expect(countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], {
countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], { '1.1.1.1': 1,
'1.1.1.1': 1, '2.2.2.2': 2,
'2.2.2.2': 2, '3.3.3.3': 3,
'3.3.3.3': 3, })).toStrictEqual(1 + 2 + 3);
}),
).toStrictEqual(1 + 2 + 3);
}); });
test('mac', () => { test('mac', () => {
expect( expect(countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], {
countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], { '1.1.1.1': 1,
'1.1.1.1': 1, '2.2.2.2': 2,
'2.2.2.2': 2, '3.3.3.3': 3,
'3.3.3.3': 3, })).toStrictEqual(2 + 3);
}),
).toStrictEqual(2 + 3);
}); });
test('not found', () => { test('not found', () => {
expect( expect(countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], {
countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], { '1.1.1.1': 1,
'1.1.1.1': 1, '2.2.2.2': 2,
'2.2.2.2': 2, '3.3.3.3': 3,
'3.3.3.3': 3, })).toStrictEqual(0);
}),
).toStrictEqual(0);
}); });
}); });
@@ -457,12 +451,10 @@ describe('subnetMaskToBitMask', () => {
test('correct for all subnetMasks', () => { test('correct for all subnetMasks', () => {
expect( expect(
subnetMasks subnetMasks.map((subnetMask) => {
.map((subnetMask) => { const bitmask = subnetMaskToBitMask(subnetMask);
const bitmask = subnetMaskToBitMask(subnetMask); return subnetMasks[bitmask] === subnetMask;
return subnetMasks[bitmask] === subnetMask; }).every((res) => res === true),
})
.every((res) => res === true),
).toEqual(true); ).toEqual(true);
}); });
}); });

View File

@@ -3,14 +3,13 @@ import i18next from 'i18next';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
import { splitByNewLine } from '../helpers/helpers'; import { splitByNewLine } from '../helpers/helpers';
export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST'); export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST');
export const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE'); export const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE');
export const getAccessListSuccess = createAction('GET_ACCESS_LIST_SUCCESS'); export const getAccessListSuccess = createAction('GET_ACCESS_LIST_SUCCESS');
export const getAccessList = () => async (dispatch: any) => { export const getAccessList = () => async (dispatch) => {
dispatch(getAccessListRequest()); dispatch(getAccessListRequest());
try { try {
const data = await apiClient.getAccessList(); const data = await apiClient.getAccessList();
@@ -25,7 +24,7 @@ export const setAccessListRequest = createAction('SET_ACCESS_LIST_REQUEST');
export const setAccessListFailure = createAction('SET_ACCESS_LIST_FAILURE'); export const setAccessListFailure = createAction('SET_ACCESS_LIST_FAILURE');
export const setAccessListSuccess = createAction('SET_ACCESS_LIST_SUCCESS'); export const setAccessListSuccess = createAction('SET_ACCESS_LIST_SUCCESS');
export const setAccessList = (config: any) => async (dispatch: any) => { export const setAccessList = (config) => async (dispatch) => {
dispatch(setAccessListRequest()); dispatch(setAccessListRequest());
try { try {
const { allowed_clients, disallowed_clients, blocked_hosts } = config; const { allowed_clients, disallowed_clients, blocked_hosts } = config;
@@ -49,7 +48,7 @@ export const toggleClientBlockRequest = createAction('TOGGLE_CLIENT_BLOCK_REQUES
export const toggleClientBlockFailure = createAction('TOGGLE_CLIENT_BLOCK_FAILURE'); export const toggleClientBlockFailure = createAction('TOGGLE_CLIENT_BLOCK_FAILURE');
export const toggleClientBlockSuccess = createAction('TOGGLE_CLIENT_BLOCK_SUCCESS'); export const toggleClientBlockSuccess = createAction('TOGGLE_CLIENT_BLOCK_SUCCESS');
export const toggleClientBlock = (ip: any, disallowed: any, disallowed_rule: any) => async (dispatch: any) => { export const toggleClientBlock = (ip, disallowed, disallowed_rule) => async (dispatch) => {
dispatch(toggleClientBlockRequest()); dispatch(toggleClientBlockRequest());
try { try {
const accessList = await apiClient.getAccessList(); const accessList = await apiClient.getAccessList();
@@ -61,10 +60,12 @@ export const toggleClientBlock = (ip: any, disallowed: any, disallowed_rule: any
if (!disallowed_rule) { if (!disallowed_rule) {
allowed_clients = allowed_clients.concat(ip); allowed_clients = allowed_clients.concat(ip);
} else { } else {
disallowed_clients = disallowed_clients.filter((client: any) => client !== disallowed_rule); disallowed_clients = disallowed_clients
.filter((client) => client !== disallowed_rule);
} }
} else if (allowed_clients.length > 1) { } else if (allowed_clients.length > 1) {
allowed_clients = allowed_clients.filter((client: any) => client !== disallowed_rule); allowed_clients = allowed_clients
.filter((client) => client !== disallowed_rule);
} else { } else {
disallowed_clients = disallowed_clients.concat(ip); disallowed_clients = disallowed_clients.concat(ip);
} }

View File

@@ -1,7 +1,6 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import i18next from 'i18next'; import i18next from 'i18next';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { getClients } from './index'; import { getClients } from './index';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
@@ -11,7 +10,7 @@ export const addClientRequest = createAction('ADD_CLIENT_REQUEST');
export const addClientFailure = createAction('ADD_CLIENT_FAILURE'); export const addClientFailure = createAction('ADD_CLIENT_FAILURE');
export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS'); export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS');
export const addClient = (config: any) => async (dispatch: any) => { export const addClient = (config) => async (dispatch) => {
dispatch(addClientRequest()); dispatch(addClientRequest());
try { try {
await apiClient.addClient(config); await apiClient.addClient(config);
@@ -29,7 +28,7 @@ export const deleteClientRequest = createAction('DELETE_CLIENT_REQUEST');
export const deleteClientFailure = createAction('DELETE_CLIENT_FAILURE'); export const deleteClientFailure = createAction('DELETE_CLIENT_FAILURE');
export const deleteClientSuccess = createAction('DELETE_CLIENT_SUCCESS'); export const deleteClientSuccess = createAction('DELETE_CLIENT_SUCCESS');
export const deleteClient = (config: any) => async (dispatch: any) => { export const deleteClient = (config) => async (dispatch) => {
dispatch(deleteClientRequest()); dispatch(deleteClientRequest());
try { try {
await apiClient.deleteClient(config); await apiClient.deleteClient(config);
@@ -46,7 +45,7 @@ export const updateClientRequest = createAction('UPDATE_CLIENT_REQUEST');
export const updateClientFailure = createAction('UPDATE_CLIENT_FAILURE'); export const updateClientFailure = createAction('UPDATE_CLIENT_FAILURE');
export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS'); export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS');
export const updateClient = (config: any, name: any) => async (dispatch: any) => { export const updateClient = (config, name) => async (dispatch) => {
dispatch(updateClientRequest()); dispatch(updateClientRequest());
try { try {
const data = { name, data: { ...config } }; const data = { name, data: { ...config } };

View File

@@ -2,7 +2,6 @@ import { createAction } from 'redux-actions';
import i18next from 'i18next'; import i18next from 'i18next';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { splitByNewLine } from '../helpers/helpers'; import { splitByNewLine } from '../helpers/helpers';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
@@ -10,7 +9,7 @@ export const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST');
export const getDnsConfigFailure = createAction('GET_DNS_CONFIG_FAILURE'); export const getDnsConfigFailure = createAction('GET_DNS_CONFIG_FAILURE');
export const getDnsConfigSuccess = createAction('GET_DNS_CONFIG_SUCCESS'); export const getDnsConfigSuccess = createAction('GET_DNS_CONFIG_SUCCESS');
export const getDnsConfig = () => async (dispatch: any) => { export const getDnsConfig = () => async (dispatch) => {
dispatch(getDnsConfigRequest()); dispatch(getDnsConfigRequest());
try { try {
const data = await apiClient.getDnsConfig(); const data = await apiClient.getDnsConfig();
@@ -25,7 +24,7 @@ export const clearDnsCacheRequest = createAction('CLEAR_DNS_CACHE_REQUEST');
export const clearDnsCacheFailure = createAction('CLEAR_DNS_CACHE_FAILURE'); export const clearDnsCacheFailure = createAction('CLEAR_DNS_CACHE_FAILURE');
export const clearDnsCacheSuccess = createAction('CLEAR_DNS_CACHE_SUCCESS'); export const clearDnsCacheSuccess = createAction('CLEAR_DNS_CACHE_SUCCESS');
export const clearDnsCache = () => async (dispatch: any) => { export const clearDnsCache = () => async (dispatch) => {
dispatch(clearDnsCacheRequest()); dispatch(clearDnsCacheRequest());
try { try {
const data = await apiClient.clearCache(); const data = await apiClient.clearCache();
@@ -41,7 +40,7 @@ export const setDnsConfigRequest = createAction('SET_DNS_CONFIG_REQUEST');
export const setDnsConfigFailure = createAction('SET_DNS_CONFIG_FAILURE'); export const setDnsConfigFailure = createAction('SET_DNS_CONFIG_FAILURE');
export const setDnsConfigSuccess = createAction('SET_DNS_CONFIG_SUCCESS'); export const setDnsConfigSuccess = createAction('SET_DNS_CONFIG_SUCCESS');
export const setDnsConfig = (config: any) => async (dispatch: any) => { export const setDnsConfig = (config) => async (dispatch) => {
dispatch(setDnsConfigRequest()); dispatch(setDnsConfigRequest());
try { try {
const data = { ...config }; const data = { ...config };

View File

@@ -1,6 +1,5 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { redirectToCurrentProtocol } from '../helpers/helpers'; import { redirectToCurrentProtocol } from '../helpers/helpers';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
@@ -8,7 +7,7 @@ export const getTlsStatusRequest = createAction('GET_TLS_STATUS_REQUEST');
export const getTlsStatusFailure = createAction('GET_TLS_STATUS_FAILURE'); export const getTlsStatusFailure = createAction('GET_TLS_STATUS_FAILURE');
export const getTlsStatusSuccess = createAction('GET_TLS_STATUS_SUCCESS'); export const getTlsStatusSuccess = createAction('GET_TLS_STATUS_SUCCESS');
export const getTlsStatus = () => async (dispatch: any) => { export const getTlsStatus = () => async (dispatch) => {
dispatch(getTlsStatusRequest()); dispatch(getTlsStatusRequest());
try { try {
const status = await apiClient.getTlsStatus(); const status = await apiClient.getTlsStatus();
@@ -27,7 +26,7 @@ export const setTlsConfigFailure = createAction('SET_TLS_CONFIG_FAILURE');
export const setTlsConfigSuccess = createAction('SET_TLS_CONFIG_SUCCESS'); export const setTlsConfigSuccess = createAction('SET_TLS_CONFIG_SUCCESS');
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS'); export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
export const setTlsConfig = (config: any) => async (dispatch: any, getState: any) => { export const setTlsConfig = (config) => async (dispatch, getState) => {
dispatch(setTlsConfigRequest()); dispatch(setTlsConfigRequest());
try { try {
const { httpPort } = getState().dashboard; const { httpPort } = getState().dashboard;
@@ -68,7 +67,7 @@ export const validateTlsConfigRequest = createAction('VALIDATE_TLS_CONFIG_REQUES
export const validateTlsConfigFailure = createAction('VALIDATE_TLS_CONFIG_FAILURE'); export const validateTlsConfigFailure = createAction('VALIDATE_TLS_CONFIG_FAILURE');
export const validateTlsConfigSuccess = createAction('VALIDATE_TLS_CONFIG_SUCCESS'); export const validateTlsConfigSuccess = createAction('VALIDATE_TLS_CONFIG_SUCCESS');
export const validateTlsConfig = (config: any) => async (dispatch: any) => { export const validateTlsConfig = (config) => async (dispatch) => {
dispatch(validateTlsConfigRequest()); dispatch(validateTlsConfigRequest());
try { try {
const values = { ...config }; const values = { ...config };

View File

@@ -13,7 +13,7 @@ export const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQU
export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE'); export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE');
export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS'); export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS');
export const getFilteringStatus = () => async (dispatch: any) => { export const getFilteringStatus = () => async (dispatch) => {
dispatch(getFilteringStatusRequest()); dispatch(getFilteringStatusRequest());
try { try {
const status = await apiClient.getFilteringStatus(); const status = await apiClient.getFilteringStatus();
@@ -28,7 +28,7 @@ export const setRulesRequest = createAction('SET_RULES_REQUEST');
export const setRulesFailure = createAction('SET_RULES_FAILURE'); export const setRulesFailure = createAction('SET_RULES_FAILURE');
export const setRulesSuccess = createAction('SET_RULES_SUCCESS'); export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
export const setRules = (rules: any) => async (dispatch: any) => { export const setRules = (rules) => async (dispatch) => {
dispatch(setRulesRequest()); dispatch(setRulesRequest());
try { try {
const normalizedRules = { const normalizedRules = {
@@ -47,91 +47,83 @@ export const addFilterRequest = createAction('ADD_FILTER_REQUEST');
export const addFilterFailure = createAction('ADD_FILTER_FAILURE'); export const addFilterFailure = createAction('ADD_FILTER_FAILURE');
export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS'); export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');
export const addFilter = export const addFilter = (url, name, whitelist = false) => async (dispatch, getState) => {
(url: any, name: any, whitelist = false) => dispatch(addFilterRequest());
async (dispatch: any, getState: any) => { try {
dispatch(addFilterRequest()); await apiClient.addFilter({ url, name, whitelist });
try { dispatch(addFilterSuccess(url));
await apiClient.addFilter({ url, name, whitelist }); if (getState().filtering.isModalOpen) {
dispatch(addFilterSuccess(url)); dispatch(toggleFilteringModal());
if (getState().filtering.isModalOpen) {
dispatch(toggleFilteringModal());
}
dispatch(addSuccessToast('filter_added_successfully'));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(addFilterFailure());
} }
}; dispatch(addSuccessToast('filter_added_successfully'));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(addFilterFailure());
}
};
export const removeFilterRequest = createAction('REMOVE_FILTER_REQUEST'); export const removeFilterRequest = createAction('REMOVE_FILTER_REQUEST');
export const removeFilterFailure = createAction('REMOVE_FILTER_FAILURE'); export const removeFilterFailure = createAction('REMOVE_FILTER_FAILURE');
export const removeFilterSuccess = createAction('REMOVE_FILTER_SUCCESS'); export const removeFilterSuccess = createAction('REMOVE_FILTER_SUCCESS');
export const removeFilter = export const removeFilter = (url, whitelist = false) => async (dispatch, getState) => {
(url: any, whitelist = false) => dispatch(removeFilterRequest());
async (dispatch: any, getState: any) => { try {
dispatch(removeFilterRequest()); await apiClient.removeFilter({ url, whitelist });
try { dispatch(removeFilterSuccess(url));
await apiClient.removeFilter({ url, whitelist }); if (getState().filtering.isModalOpen) {
dispatch(removeFilterSuccess(url)); dispatch(toggleFilteringModal());
if (getState().filtering.isModalOpen) {
dispatch(toggleFilteringModal());
}
dispatch(addSuccessToast('filter_removed_successfully'));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(removeFilterFailure());
} }
}; dispatch(addSuccessToast('filter_removed_successfully'));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(removeFilterFailure());
}
};
export const toggleFilterRequest = createAction('FILTER_TOGGLE_REQUEST'); export const toggleFilterRequest = createAction('FILTER_TOGGLE_REQUEST');
export const toggleFilterFailure = createAction('FILTER_TOGGLE_FAILURE'); export const toggleFilterFailure = createAction('FILTER_TOGGLE_FAILURE');
export const toggleFilterSuccess = createAction('FILTER_TOGGLE_SUCCESS'); export const toggleFilterSuccess = createAction('FILTER_TOGGLE_SUCCESS');
export const toggleFilterStatus = export const toggleFilterStatus = (url, data, whitelist = false) => async (dispatch) => {
(url: any, data: any, whitelist = false) => dispatch(toggleFilterRequest());
async (dispatch: any) => { try {
dispatch(toggleFilterRequest()); await apiClient.setFilterUrl({ url, data, whitelist });
try { dispatch(toggleFilterSuccess(url));
await apiClient.setFilterUrl({ url, data, whitelist }); dispatch(getFilteringStatus());
dispatch(toggleFilterSuccess(url)); } catch (error) {
dispatch(getFilteringStatus()); dispatch(addErrorToast({ error }));
} catch (error) { dispatch(toggleFilterFailure());
dispatch(addErrorToast({ error })); }
dispatch(toggleFilterFailure()); };
}
};
export const editFilterRequest = createAction('EDIT_FILTER_REQUEST'); export const editFilterRequest = createAction('EDIT_FILTER_REQUEST');
export const editFilterFailure = createAction('EDIT_FILTER_FAILURE'); export const editFilterFailure = createAction('EDIT_FILTER_FAILURE');
export const editFilterSuccess = createAction('EDIT_FILTER_SUCCESS'); export const editFilterSuccess = createAction('EDIT_FILTER_SUCCESS');
export const editFilter = export const editFilter = (url, data, whitelist = false) => async (dispatch, getState) => {
(url: any, data: any, whitelist = false) => dispatch(editFilterRequest());
async (dispatch: any, getState: any) => { try {
dispatch(editFilterRequest()); await apiClient.setFilterUrl({ url, data, whitelist });
try { dispatch(editFilterSuccess(url));
await apiClient.setFilterUrl({ url, data, whitelist }); if (getState().filtering.isModalOpen) {
dispatch(editFilterSuccess(url)); dispatch(toggleFilteringModal());
if (getState().filtering.isModalOpen) {
dispatch(toggleFilteringModal());
}
dispatch(addSuccessToast('filter_updated'));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(editFilterFailure());
} }
}; dispatch(addSuccessToast('filter_updated'));
dispatch(getFilteringStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(editFilterFailure());
}
};
export const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST'); export const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST');
export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE'); export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE');
export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS'); export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
export const refreshFilters = (config: any) => async (dispatch: any) => { export const refreshFilters = (config) => async (dispatch) => {
dispatch(refreshFiltersRequest()); dispatch(refreshFiltersRequest());
dispatch(showLoading()); dispatch(showLoading());
try { try {
@@ -158,7 +150,7 @@ export const setFiltersConfigRequest = createAction('SET_FILTERS_CONFIG_REQUEST'
export const setFiltersConfigFailure = createAction('SET_FILTERS_CONFIG_FAILURE'); export const setFiltersConfigFailure = createAction('SET_FILTERS_CONFIG_FAILURE');
export const setFiltersConfigSuccess = createAction('SET_FILTERS_CONFIG_SUCCESS'); export const setFiltersConfigSuccess = createAction('SET_FILTERS_CONFIG_SUCCESS');
export const setFiltersConfig = (config: any) => async (dispatch: any, getState: any) => { export const setFiltersConfig = (config) => async (dispatch, getState) => {
dispatch(setFiltersConfigRequest()); dispatch(setFiltersConfigRequest());
try { try {
const { enabled } = config; const { enabled } = config;
@@ -188,18 +180,16 @@ export const checkHostSuccess = createAction('CHECK_HOST_SUCCESS');
* @param {string} host.name * @param {string} host.name
* @returns {undefined} * @returns {undefined}
*/ */
export const checkHost = (host: any) => async (dispatch: any) => { export const checkHost = (host) => async (dispatch) => {
dispatch(checkHostRequest()); dispatch(checkHostRequest());
try { try {
const data = await apiClient.checkHost(host); const data = await apiClient.checkHost(host);
const { name: hostname } = host; const { name: hostname } = host;
dispatch( dispatch(checkHostSuccess({
checkHostSuccess({ hostname,
hostname, ...data,
...data, }));
}),
);
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(checkHostFailure()); dispatch(checkHostFailure());

View File

@@ -38,7 +38,7 @@ export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
* @param {*} status: boolean | SafeSearchConfig * @param {*} status: boolean | SafeSearchConfig
* @returns * @returns
*/ */
export const toggleSetting = (settingKey: any, status: any) => async (dispatch: any) => { export const toggleSetting = (settingKey, status) => async (dispatch) => {
let successMessage = ''; let successMessage = '';
try { try {
switch (settingKey) { switch (settingKey) {
@@ -80,58 +80,64 @@ export const initSettingsRequest = createAction('SETTINGS_INIT_REQUEST');
export const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE'); export const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE');
export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS'); export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS');
export const initSettings = export const initSettings = (settingsList = {
( safebrowsing: {}, parental: {},
settingsList = { }) => async (dispatch) => {
safebrowsing: {}, dispatch(initSettingsRequest());
parental: {}, try {
}, const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
) => const parentalStatus = await apiClient.getParentalStatus();
async (dispatch: any) => { const safesearchStatus = await apiClient.getSafesearchStatus();
dispatch(initSettingsRequest()); const {
try { safebrowsing,
const safebrowsingStatus = await apiClient.getSafebrowsingStatus(); parental,
const parentalStatus = await apiClient.getParentalStatus(); } = settingsList;
const safesearchStatus = await apiClient.getSafesearchStatus(); const newSettingsList = {
const { safebrowsing, parental } = settingsList; safebrowsing: {
const newSettingsList = { ...safebrowsing,
safebrowsing: { enabled: safebrowsingStatus.enabled,
...safebrowsing, },
enabled: safebrowsingStatus.enabled, parental: {
}, ...parental,
parental: { enabled: parentalStatus.enabled,
...parental, },
enabled: parentalStatus.enabled, safesearch: {
}, ...safesearchStatus,
safesearch: { },
...safesearchStatus, };
}, dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
}; } catch (error) {
dispatch(initSettingsSuccess({ settingsList: newSettingsList })); dispatch(addErrorToast({ error }));
} catch (error) { dispatch(initSettingsFailure());
dispatch(addErrorToast({ error })); }
dispatch(initSettingsFailure()); };
}
};
export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST'); export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE'); export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS'); export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
const getDisabledMessage = (time: any) => { const getDisabledMessage = (time) => {
switch (time) { switch (time) {
case DISABLE_PROTECTION_TIMINGS.HALF_MINUTE: case DISABLE_PROTECTION_TIMINGS.HALF_MINUTE:
return i18next.t('disable_notify_for_seconds', { return i18next.t(
count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE), 'disable_notify_for_seconds',
}); { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) },
);
case DISABLE_PROTECTION_TIMINGS.MINUTE: case DISABLE_PROTECTION_TIMINGS.MINUTE:
return i18next.t('disable_notify_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }); return i18next.t(
'disable_notify_for_minutes',
{ count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) },
);
case DISABLE_PROTECTION_TIMINGS.TEN_MINUTES: case DISABLE_PROTECTION_TIMINGS.TEN_MINUTES:
return i18next.t('disable_notify_for_minutes', { return i18next.t(
count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES), 'disable_notify_for_minutes',
}); { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) },
);
case DISABLE_PROTECTION_TIMINGS.HOUR: case DISABLE_PROTECTION_TIMINGS.HOUR:
return i18next.t('disable_notify_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }); return i18next.t(
'disable_notify_for_hours',
{ count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) },
);
case DISABLE_PROTECTION_TIMINGS.TOMORROW: case DISABLE_PROTECTION_TIMINGS.TOMORROW:
return i18next.t('disable_notify_until_tomorrow'); return i18next.t('disable_notify_until_tomorrow');
default: default:
@@ -139,24 +145,22 @@ const getDisabledMessage = (time: any) => {
} }
}; };
export const toggleProtection = export const toggleProtection = (status, time = null) => async (dispatch) => {
(status: any, time = null) => dispatch(toggleProtectionRequest());
async (dispatch: any) => { try {
dispatch(toggleProtectionRequest()); const successMessage = status ? getDisabledMessage(time) : 'enabled_protection';
try { await apiClient.setProtection({ enabled: !status, duration: time });
const successMessage = status ? getDisabledMessage(time) : 'enabled_protection'; dispatch(addSuccessToast(successMessage));
await apiClient.setProtection({ enabled: !status, duration: time }); dispatch(toggleProtectionSuccess({ disabledDuration: time }));
dispatch(addSuccessToast(successMessage)); } catch (error) {
dispatch(toggleProtectionSuccess({ disabledDuration: time })); dispatch(addErrorToast({ error }));
} catch (error) { dispatch(toggleProtectionFailure());
dispatch(addErrorToast({ error })); }
dispatch(toggleProtectionFailure()); };
}
};
export const setDisableDurationTime = createAction('SET_DISABLED_DURATION_TIME'); export const setDisableDurationTime = createAction('SET_DISABLED_DURATION_TIME');
export const setProtectionTimerTime = (updatedTime: any) => async (dispatch: any) => { export const setProtectionTimerTime = (updatedTime) => async (dispatch) => {
dispatch(setDisableDurationTime({ timeToEnableProtection: updatedTime })); dispatch(setDisableDurationTime({ timeToEnableProtection: updatedTime }));
}; };
@@ -164,42 +168,40 @@ export const getVersionRequest = createAction('GET_VERSION_REQUEST');
export const getVersionFailure = createAction('GET_VERSION_FAILURE'); export const getVersionFailure = createAction('GET_VERSION_FAILURE');
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS'); export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
export const getVersion = export const getVersion = (recheck = false) => async (dispatch, getState) => {
(recheck = false) => dispatch(getVersionRequest());
async (dispatch: any, getState: any) => { try {
dispatch(getVersionRequest()); const data = await apiClient.getGlobalVersion({ recheck_now: recheck });
try { dispatch(getVersionSuccess(data));
const data = await apiClient.getGlobalVersion({ recheck_now: recheck });
dispatch(getVersionSuccess(data));
if (recheck) { if (recheck) {
const { dnsVersion } = getState().dashboard; const { dnsVersion } = getState().dashboard;
const currentVersion = dnsVersion === 'undefined' ? 0 : dnsVersion; const currentVersion = dnsVersion === 'undefined' ? 0 : dnsVersion;
if (data && !areEqualVersions(currentVersion, data.new_version)) { if (data && !areEqualVersions(currentVersion, data.new_version)) {
dispatch(addSuccessToast('updates_checked')); dispatch(addSuccessToast('updates_checked'));
} else { } else {
dispatch(addSuccessToast('updates_version_equal')); dispatch(addSuccessToast('updates_version_equal'));
}
} }
} catch (error) {
dispatch(addErrorToast({ error: 'version_request_error' }));
dispatch(getVersionFailure());
} }
}; } catch (error) {
dispatch(addErrorToast({ error: 'version_request_error' }));
dispatch(getVersionFailure());
}
};
export const getUpdateRequest = createAction('GET_UPDATE_REQUEST'); export const getUpdateRequest = createAction('GET_UPDATE_REQUEST');
export const getUpdateFailure = createAction('GET_UPDATE_FAILURE'); export const getUpdateFailure = createAction('GET_UPDATE_FAILURE');
export const getUpdateSuccess = createAction('GET_UPDATE_SUCCESS'); export const getUpdateSuccess = createAction('GET_UPDATE_SUCCESS');
const checkStatus = async (handleRequestSuccess: any, handleRequestError: any, attempts = 60) => { const checkStatus = async (handleRequestSuccess, handleRequestError, attempts = 60) => {
let timeout; let timeout;
if (attempts === 0) { if (attempts === 0) {
handleRequestError(); handleRequestError();
} }
const rmTimeout = (t: any) => t && clearTimeout(t); const rmTimeout = (t) => t && clearTimeout(t);
try { try {
const response = await axios.get(`${apiClient.baseUrl}/status`); const response = await axios.get(`${apiClient.baseUrl}/status`);
@@ -218,18 +220,25 @@ const checkStatus = async (handleRequestSuccess: any, handleRequestError: any, a
} }
} catch (error) { } catch (error) {
rmTimeout(timeout); rmTimeout(timeout);
timeout = setTimeout(checkStatus, CHECK_TIMEOUT, handleRequestSuccess, handleRequestError, attempts - 1); timeout = setTimeout(
checkStatus,
CHECK_TIMEOUT,
handleRequestSuccess,
handleRequestError,
attempts - 1,
);
} }
}; };
export const getUpdate = () => async (dispatch: any, getState: any) => { export const getUpdate = () => async (dispatch, getState) => {
const { dnsVersion } = getState().dashboard; const { dnsVersion } = getState().dashboard;
dispatch(getUpdateRequest()); dispatch(getUpdateRequest());
const handleRequestError = () => { const handleRequestError = () => {
const options = { const options = {
components: { components: {
a: <a href={MANUAL_UPDATE_LINK} target="_blank" rel="noopener noreferrer" />, a: <a href={MANUAL_UPDATE_LINK} target="_blank"
rel="noopener noreferrer" />,
}, },
}; };
@@ -237,13 +246,12 @@ export const getUpdate = () => async (dispatch: any, getState: any) => {
dispatch(getUpdateFailure()); dispatch(getUpdateFailure());
}; };
const handleRequestSuccess = (response: any) => { const handleRequestSuccess = (response) => {
const responseVersion = response.data?.version; const responseVersion = response.data?.version;
if (dnsVersion !== responseVersion) { if (dnsVersion !== responseVersion) {
dispatch(getUpdateSuccess()); dispatch(getUpdateSuccess());
window.location.reload(true);
window.location.reload();
} }
}; };
@@ -259,20 +267,18 @@ export const getClientsRequest = createAction('GET_CLIENTS_REQUEST');
export const getClientsFailure = createAction('GET_CLIENTS_FAILURE'); export const getClientsFailure = createAction('GET_CLIENTS_FAILURE');
export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS'); export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
export const getClients = () => async (dispatch: any) => { export const getClients = () => async (dispatch) => {
dispatch(getClientsRequest()); dispatch(getClientsRequest());
try { try {
const data = await apiClient.getClients(); const data = await apiClient.getClients();
const sortedClients = data.clients && sortClients(data.clients); const sortedClients = data.clients && sortClients(data.clients);
const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients); const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients);
dispatch( dispatch(getClientsSuccess({
getClientsSuccess({ clients: sortedClients || [],
clients: sortedClients || [], autoClients: sortedAutoClients || [],
autoClients: sortedAutoClients || [], supportedTags: data.supported_tags || [],
supportedTags: data.supported_tags || [], }));
}),
);
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(getClientsFailure()); dispatch(getClientsFailure());
@@ -283,7 +289,7 @@ export const getProfileRequest = createAction('GET_PROFILE_REQUEST');
export const getProfileFailure = createAction('GET_PROFILE_FAILURE'); export const getProfileFailure = createAction('GET_PROFILE_FAILURE');
export const getProfileSuccess = createAction('GET_PROFILE_SUCCESS'); export const getProfileSuccess = createAction('GET_PROFILE_SUCCESS');
export const getProfile = () => async (dispatch: any) => { export const getProfile = () => async (dispatch) => {
dispatch(getProfileRequest()); dispatch(getProfileRequest());
try { try {
const profile = await apiClient.getProfile(); const profile = await apiClient.getProfile();
@@ -299,17 +305,16 @@ export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS'); export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
export const setDnsRunningStatus = createAction('SET_DNS_RUNNING_STATUS'); export const setDnsRunningStatus = createAction('SET_DNS_RUNNING_STATUS');
export const getDnsStatus = () => async (dispatch: any) => { export const getDnsStatus = () => async (dispatch) => {
dispatch(dnsStatusRequest()); dispatch(dnsStatusRequest());
const handleRequestError = () => { const handleRequestError = () => {
dispatch(addErrorToast({ error: 'dns_status_error' })); dispatch(addErrorToast({ error: 'dns_status_error' }));
dispatch(dnsStatusFailure()); dispatch(dnsStatusFailure());
window.location.reload(true);
window.location.reload();
}; };
const handleRequestSuccess = (response: any) => { const handleRequestSuccess = (response) => {
const dnsStatus = response.data; const dnsStatus = response.data;
if (dnsStatus.protection_disabled_duration === 0) { if (dnsStatus.protection_disabled_duration === 0) {
dnsStatus.protection_disabled_duration = null; dnsStatus.protection_disabled_duration = null;
@@ -337,17 +342,16 @@ export const timerStatusRequest = createAction('TIMER_STATUS_REQUEST');
export const timerStatusFailure = createAction('TIMER_STATUS_FAILURE'); export const timerStatusFailure = createAction('TIMER_STATUS_FAILURE');
export const timerStatusSuccess = createAction('TIMER_STATUS_SUCCESS'); export const timerStatusSuccess = createAction('TIMER_STATUS_SUCCESS');
export const getTimerStatus = () => async (dispatch: any) => { export const getTimerStatus = () => async (dispatch) => {
dispatch(timerStatusRequest()); dispatch(timerStatusRequest());
const handleRequestError = () => { const handleRequestError = () => {
dispatch(addErrorToast({ error: 'dns_status_error' })); dispatch(addErrorToast({ error: 'dns_status_error' }));
dispatch(dnsStatusFailure()); dispatch(dnsStatusFailure());
window.location.reload(true);
window.location.reload();
}; };
const handleRequestSuccess = (response: any) => { const handleRequestSuccess = (response) => {
const dnsStatus = response.data; const dnsStatus = response.data;
if (dnsStatus.protection_disabled_duration === 0) { if (dnsStatus.protection_disabled_duration === 0) {
dnsStatus.protection_disabled_duration = null; dnsStatus.protection_disabled_duration = null;
@@ -372,26 +376,30 @@ export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE'); export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS'); export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
export const testUpstream = export const testUpstream = (
({ bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns }: any, upstream_dns_file: any) => {
async (dispatch: any) => { bootstrap_dns,
dispatch(testUpstreamRequest()); upstream_dns,
try { local_ptr_upstreams,
const removeComments = compose(filterOutComments, splitByNewLine); fallback_dns,
}, upstream_dns_file,
) => async (dispatch) => {
dispatch(testUpstreamRequest());
try {
const removeComments = compose(filterOutComments, splitByNewLine);
const config = { const config = {
bootstrap_dns: splitByNewLine(bootstrap_dns), bootstrap_dns: splitByNewLine(bootstrap_dns),
private_upstream: splitByNewLine(local_ptr_upstreams), private_upstream: splitByNewLine(local_ptr_upstreams),
fallback_dns: splitByNewLine(fallback_dns), fallback_dns: splitByNewLine(fallback_dns),
...(upstream_dns_file ...(upstream_dns_file ? null : {
? null upstream_dns: removeComments(upstream_dns),
: { }),
upstream_dns: removeComments(upstream_dns), };
}),
};
const upstreamResponse = await apiClient.testUpstream(config); const upstreamResponse = await apiClient.testUpstream(config);
const testMessages = Object.keys(upstreamResponse).map((key) => { const testMessages = Object.keys(upstreamResponse)
.map((key) => {
const message = upstreamResponse[key]; const message = upstreamResponse[key];
if (message.startsWith('WARNING:')) { if (message.startsWith('WARNING:')) {
dispatch(addErrorToast({ error: i18next.t('dns_test_warning_toast', { key }) })); dispatch(addErrorToast({ error: i18next.t('dns_test_warning_toast', { key }) }));
@@ -399,54 +407,46 @@ export const testUpstream =
const info = message.substring(0, message.indexOf(':')); const info = message.substring(0, message.indexOf(':'));
const [sectionKey, line] = info.split(' '); const [sectionKey, line] = info.split(' ');
const section = i18next.t(sectionKey); const section = i18next.t(sectionKey);
dispatch( dispatch(addErrorToast({ error: i18next.t('dns_test_parsing_error_toast', { section, line }) }));
addErrorToast({
error: i18next.t('dns_test_parsing_error_toast', {
section,
line,
}),
}),
);
} else if (message !== 'OK') { } else if (message !== 'OK') {
dispatch(addErrorToast({ error: i18next.t('dns_test_not_ok_toast', { key }) })); dispatch(addErrorToast({ error: i18next.t('dns_test_not_ok_toast', { key }) }));
} }
return message; return message;
}); });
if (testMessages.every((message) => message === 'OK' || message.startsWith('WARNING:'))) { if (testMessages.every((message) => message === 'OK' || message.startsWith('WARNING:'))) {
dispatch(addSuccessToast('dns_test_ok_toast')); dispatch(addSuccessToast('dns_test_ok_toast'));
}
dispatch(testUpstreamSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(testUpstreamFailure());
} }
};
export const testUpstreamWithFormValues = () => async (dispatch: any, getState: any) => { dispatch(testUpstreamSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(testUpstreamFailure());
}
};
export const testUpstreamWithFormValues = () => async (dispatch, getState) => {
const { upstream_dns_file } = getState().dnsConfig; const { upstream_dns_file } = getState().dnsConfig;
const { bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns } = const {
getState().form[FORM_NAME.UPSTREAM].values; bootstrap_dns,
upstream_dns,
local_ptr_upstreams,
fallback_dns,
} = getState().form[FORM_NAME.UPSTREAM].values;
return dispatch( return dispatch(testUpstream({
testUpstream( bootstrap_dns,
{ upstream_dns,
bootstrap_dns, local_ptr_upstreams,
upstream_dns, fallback_dns,
local_ptr_upstreams, }, upstream_dns_file));
fallback_dns,
},
upstream_dns_file,
),
);
}; };
export const changeLanguageRequest = createAction('CHANGE_LANGUAGE_REQUEST'); export const changeLanguageRequest = createAction('CHANGE_LANGUAGE_REQUEST');
export const changeLanguageFailure = createAction('CHANGE_LANGUAGE_FAILURE'); export const changeLanguageFailure = createAction('CHANGE_LANGUAGE_FAILURE');
export const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS'); export const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS');
export const changeLanguage = (lang: any) => async (dispatch: any) => { export const changeLanguage = (lang) => async (dispatch) => {
dispatch(changeLanguageRequest()); dispatch(changeLanguageRequest());
try { try {
await apiClient.changeLanguage({ language: lang }); await apiClient.changeLanguage({ language: lang });
@@ -461,7 +461,7 @@ export const changeThemeRequest = createAction('CHANGE_THEME_REQUEST');
export const changeThemeFailure = createAction('CHANGE_THEME_FAILURE'); export const changeThemeFailure = createAction('CHANGE_THEME_FAILURE');
export const changeThemeSuccess = createAction('CHANGE_THEME_SUCCESS'); export const changeThemeSuccess = createAction('CHANGE_THEME_SUCCESS');
export const changeTheme = (theme: any) => async (dispatch: any) => { export const changeTheme = (theme) => async (dispatch) => {
dispatch(changeThemeRequest()); dispatch(changeThemeRequest());
try { try {
await apiClient.changeTheme({ theme }); await apiClient.changeTheme({ theme });
@@ -476,7 +476,7 @@ export const getDhcpStatusRequest = createAction('GET_DHCP_STATUS_REQUEST');
export const getDhcpStatusSuccess = createAction('GET_DHCP_STATUS_SUCCESS'); export const getDhcpStatusSuccess = createAction('GET_DHCP_STATUS_SUCCESS');
export const getDhcpStatusFailure = createAction('GET_DHCP_STATUS_FAILURE'); export const getDhcpStatusFailure = createAction('GET_DHCP_STATUS_FAILURE');
export const getDhcpStatus = () => async (dispatch: any) => { export const getDhcpStatus = () => async (dispatch) => {
dispatch(getDhcpStatusRequest()); dispatch(getDhcpStatusRequest());
try { try {
const globalStatus = await apiClient.getGlobalStatus(); const globalStatus = await apiClient.getGlobalStatus();
@@ -497,7 +497,7 @@ export const getDhcpInterfacesRequest = createAction('GET_DHCP_INTERFACES_REQUES
export const getDhcpInterfacesSuccess = createAction('GET_DHCP_INTERFACES_SUCCESS'); export const getDhcpInterfacesSuccess = createAction('GET_DHCP_INTERFACES_SUCCESS');
export const getDhcpInterfacesFailure = createAction('GET_DHCP_INTERFACES_FAILURE'); export const getDhcpInterfacesFailure = createAction('GET_DHCP_INTERFACES_FAILURE');
export const getDhcpInterfaces = () => async (dispatch: any) => { export const getDhcpInterfaces = () => async (dispatch) => {
dispatch(getDhcpInterfacesRequest()); dispatch(getDhcpInterfacesRequest());
try { try {
const interfaces = await apiClient.getDhcpInterfaces(); const interfaces = await apiClient.getDhcpInterfaces();
@@ -512,7 +512,7 @@ export const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST');
export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS'); export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS');
export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE'); export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
export const findActiveDhcp = (name: any) => async (dispatch: any, getState: any) => { export const findActiveDhcp = (name) => async (dispatch, getState) => {
dispatch(findActiveDhcpRequest()); dispatch(findActiveDhcpRequest());
try { try {
const req = { const req = {
@@ -559,12 +559,12 @@ export const findActiveDhcp = (name: any) => async (dispatch: any, getState: any
return; return;
} }
if ( if ((hasV4Interface && v4.other_server.found === STATUS_RESPONSE.YES)
(hasV4Interface && v4.other_server.found === STATUS_RESPONSE.YES) || || (hasV6Interface && v6.other_server.found === STATUS_RESPONSE.YES)) {
(hasV6Interface && v6.other_server.found === STATUS_RESPONSE.YES)
) {
dispatch(addErrorToast({ error: 'dhcp_found' })); dispatch(addErrorToast({ error: 'dhcp_found' }));
} else if (hasV4Interface && v4.static_ip.static === STATUS_RESPONSE.NO && v4.static_ip.ip && interface_name) { } else if (hasV4Interface && v4.static_ip.static === STATUS_RESPONSE.NO
&& v4.static_ip.ip
&& interface_name) {
const warning = i18next.t('dhcp_dynamic_ip_found', { const warning = i18next.t('dhcp_dynamic_ip_found', {
interfaceName: interface_name, interfaceName: interface_name,
ipAddress: v4.static_ip.ip, ipAddress: v4.static_ip.ip,
@@ -587,7 +587,7 @@ export const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST');
export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS'); export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS');
export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE'); export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');
export const setDhcpConfig = (values: any) => async (dispatch: any) => { export const setDhcpConfig = (values) => async (dispatch) => {
dispatch(setDhcpConfigRequest()); dispatch(setDhcpConfigRequest());
try { try {
await apiClient.setDhcpConfig(values); await apiClient.setDhcpConfig(values);
@@ -603,7 +603,7 @@ export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST');
export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE'); export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE');
export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS'); export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS');
export const toggleDhcp = (values: any) => async (dispatch: any) => { export const toggleDhcp = (values) => async (dispatch) => {
dispatch(toggleDhcpRequest()); dispatch(toggleDhcpRequest());
let config = { let config = {
...values, ...values,
@@ -633,7 +633,7 @@ export const resetDhcpRequest = createAction('RESET_DHCP_REQUEST');
export const resetDhcpSuccess = createAction('RESET_DHCP_SUCCESS'); export const resetDhcpSuccess = createAction('RESET_DHCP_SUCCESS');
export const resetDhcpFailure = createAction('RESET_DHCP_FAILURE'); export const resetDhcpFailure = createAction('RESET_DHCP_FAILURE');
export const resetDhcp = () => async (dispatch: any) => { export const resetDhcp = () => async (dispatch) => {
dispatch(resetDhcpRequest()); dispatch(resetDhcpRequest());
try { try {
const status = await apiClient.resetDhcp(); const status = await apiClient.resetDhcp();
@@ -649,7 +649,7 @@ export const resetDhcpLeasesRequest = createAction('RESET_DHCP_LEASES_REQUEST');
export const resetDhcpLeasesSuccess = createAction('RESET_DHCP_LEASES_SUCCESS'); export const resetDhcpLeasesSuccess = createAction('RESET_DHCP_LEASES_SUCCESS');
export const resetDhcpLeasesFailure = createAction('RESET_DHCP_LEASES_FAILURE'); export const resetDhcpLeasesFailure = createAction('RESET_DHCP_LEASES_FAILURE');
export const resetDhcpLeases = () => async (dispatch: any) => { export const resetDhcpLeases = () => async (dispatch) => {
dispatch(resetDhcpLeasesRequest()); dispatch(resetDhcpLeasesRequest());
try { try {
const status = await apiClient.resetDhcpLeases(); const status = await apiClient.resetDhcpLeases();
@@ -667,7 +667,7 @@ export const addStaticLeaseRequest = createAction('ADD_STATIC_LEASE_REQUEST');
export const addStaticLeaseFailure = createAction('ADD_STATIC_LEASE_FAILURE'); export const addStaticLeaseFailure = createAction('ADD_STATIC_LEASE_FAILURE');
export const addStaticLeaseSuccess = createAction('ADD_STATIC_LEASE_SUCCESS'); export const addStaticLeaseSuccess = createAction('ADD_STATIC_LEASE_SUCCESS');
export const addStaticLease = (config: any) => async (dispatch: any) => { export const addStaticLease = (config) => async (dispatch) => {
dispatch(addStaticLeaseRequest()); dispatch(addStaticLeaseRequest());
try { try {
const name = config.hostname || config.ip; const name = config.hostname || config.ip;
@@ -686,7 +686,7 @@ export const removeStaticLeaseRequest = createAction('REMOVE_STATIC_LEASE_REQUES
export const removeStaticLeaseFailure = createAction('REMOVE_STATIC_LEASE_FAILURE'); export const removeStaticLeaseFailure = createAction('REMOVE_STATIC_LEASE_FAILURE');
export const removeStaticLeaseSuccess = createAction('REMOVE_STATIC_LEASE_SUCCESS'); export const removeStaticLeaseSuccess = createAction('REMOVE_STATIC_LEASE_SUCCESS');
export const removeStaticLease = (config: any) => async (dispatch: any) => { export const removeStaticLease = (config) => async (dispatch) => {
dispatch(removeStaticLeaseRequest()); dispatch(removeStaticLeaseRequest());
try { try {
const name = config.hostname || config.ip; const name = config.hostname || config.ip;
@@ -703,7 +703,7 @@ export const updateStaticLeaseRequest = createAction('UPDATE_STATIC_LEASE_REQUES
export const updateStaticLeaseFailure = createAction('UPDATE_STATIC_LEASE_FAILURE'); export const updateStaticLeaseFailure = createAction('UPDATE_STATIC_LEASE_FAILURE');
export const updateStaticLeaseSuccess = createAction('UPDATE_STATIC_LEASE_SUCCESS'); export const updateStaticLeaseSuccess = createAction('UPDATE_STATIC_LEASE_SUCCESS');
export const updateStaticLease = (config: any) => async (dispatch: any) => { export const updateStaticLease = (config) => async (dispatch) => {
dispatch(updateStaticLeaseRequest()); dispatch(updateStaticLeaseRequest());
try { try {
await apiClient.updateStaticLease(config); await apiClient.updateStaticLease(config);
@@ -719,42 +719,42 @@ export const updateStaticLease = (config: any) => async (dispatch: any) => {
export const removeToast = createAction('REMOVE_TOAST'); export const removeToast = createAction('REMOVE_TOAST');
export const toggleBlocking = export const toggleBlocking = (
(type: any, domain: any, baseRule?: string, baseUnblocking?: string) => async (dispatch: any, getState: any) => { type, domain, baseRule, baseUnblocking,
const baseBlockingRule = baseRule || `||${domain}^$important`; ) => async (dispatch, getState) => {
const baseUnblockingRule = baseUnblocking || `@@${baseBlockingRule}`; const baseBlockingRule = baseRule || `||${domain}^$important`;
const { userRules } = getState().filtering; const baseUnblockingRule = baseUnblocking || `@@${baseBlockingRule}`;
const { userRules } = getState().filtering;
const lineEnding = !endsWith(userRules, '\n') ? '\n' : ''; const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblockingRule : baseBlockingRule; const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblockingRule : baseBlockingRule;
const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseBlockingRule : baseUnblockingRule; const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseBlockingRule : baseUnblockingRule;
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`); const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`); const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
const matchPreparedBlockingRule = userRules.match(preparedBlockingRule); const matchPreparedBlockingRule = userRules.match(preparedBlockingRule);
const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule); const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule);
if (matchPreparedBlockingRule) { if (matchPreparedBlockingRule) {
await dispatch(setRules(userRules.replace(`${blockingRule}`, ''))); await dispatch(setRules(userRules.replace(`${blockingRule}`, '')));
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule }))); dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
} else if (!matchPreparedUnblockingRule) { } else if (!matchPreparedUnblockingRule) {
await dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`)); await dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`));
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule }))); dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
} else if (matchPreparedUnblockingRule) { } else if (matchPreparedUnblockingRule) {
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule }))); dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
return; return;
} else if (!matchPreparedBlockingRule) { } else if (!matchPreparedBlockingRule) {
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule }))); dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
return; return;
} }
dispatch(getFilteringStatus()); dispatch(getFilteringStatus());
}; };
export const toggleBlockingForClient = (type: any, domain: any, client: any) => { export const toggleBlockingForClient = (type, domain, client) => {
const escapedClientName = client const escapedClientName = client.replace(/'/g, '\\\'')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"') .replace(/"/g, '\\"')
.replace(/,/g, '\\,') .replace(/,/g, '\\,')
.replace(/\|/g, '\\|'); .replace(/\|/g, '\\|');

View File

@@ -9,7 +9,7 @@ export const getDefaultAddressesRequest = createAction('GET_DEFAULT_ADDRESSES_RE
export const getDefaultAddressesFailure = createAction('GET_DEFAULT_ADDRESSES_FAILURE'); export const getDefaultAddressesFailure = createAction('GET_DEFAULT_ADDRESSES_FAILURE');
export const getDefaultAddressesSuccess = createAction('GET_DEFAULT_ADDRESSES_SUCCESS'); export const getDefaultAddressesSuccess = createAction('GET_DEFAULT_ADDRESSES_SUCCESS');
export const getDefaultAddresses = () => async (dispatch: any) => { export const getDefaultAddresses = () => async (dispatch) => {
dispatch(getDefaultAddressesRequest()); dispatch(getDefaultAddressesRequest());
try { try {
const addresses = await apiClient.getDefaultAddresses(); const addresses = await apiClient.getDefaultAddresses();
@@ -24,10 +24,13 @@ export const setAllSettingsRequest = createAction('SET_ALL_SETTINGS_REQUEST');
export const setAllSettingsFailure = createAction('SET_ALL_SETTINGS_FAILURE'); export const setAllSettingsFailure = createAction('SET_ALL_SETTINGS_FAILURE');
export const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS'); export const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS');
export const setAllSettings = (values: any) => async (dispatch: any) => { export const setAllSettings = (values) => async (dispatch) => {
dispatch(setAllSettingsRequest()); dispatch(setAllSettingsRequest());
try { try {
const { confirm_password, ...config } = values; const {
confirm_password,
...config
} = values;
await apiClient.setAllSettings(config); await apiClient.setAllSettings(config);
dispatch(setAllSettingsSuccess()); dispatch(setAllSettingsSuccess());
@@ -44,7 +47,7 @@ export const checkConfigRequest = createAction('CHECK_CONFIG_REQUEST');
export const checkConfigFailure = createAction('CHECK_CONFIG_FAILURE'); export const checkConfigFailure = createAction('CHECK_CONFIG_FAILURE');
export const checkConfigSuccess = createAction('CHECK_CONFIG_SUCCESS'); export const checkConfigSuccess = createAction('CHECK_CONFIG_SUCCESS');
export const checkConfig = (values: any) => async (dispatch: any) => { export const checkConfig = (values) => async (dispatch) => {
dispatch(checkConfigRequest()); dispatch(checkConfigRequest());
try { try {
const check = await apiClient.checkConfig(values); const check = await apiClient.checkConfig(values);

View File

@@ -8,12 +8,12 @@ export const processLoginRequest = createAction('PROCESS_LOGIN_REQUEST');
export const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE'); export const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE');
export const processLoginSuccess = createAction('PROCESS_LOGIN_SUCCESS'); export const processLoginSuccess = createAction('PROCESS_LOGIN_SUCCESS');
export const processLogin = (values: any) => async (dispatch: any) => { export const processLogin = (values) => async (dispatch) => {
dispatch(processLoginRequest()); dispatch(processLoginRequest());
try { try {
await apiClient.login(values); await apiClient.login(values);
const dashboardUrl = const dashboardUrl = window.location.origin
window.location.origin + window.location.pathname.replace(HTML_PAGES.LOGIN, HTML_PAGES.MAIN); + window.location.pathname.replace(HTML_PAGES.LOGIN, HTML_PAGES.MAIN);
window.location.replace(dashboardUrl); window.location.replace(dashboardUrl);
dispatch(processLoginSuccess()); dispatch(processLoginSuccess());
} catch (error) { } catch (error) {

View File

@@ -1,12 +1,13 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { normalizeLogs } from '../helpers/helpers'; import { normalizeLogs } from '../helpers/helpers';
import { DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT } from '../helpers/constants'; import {
DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT,
} from '../helpers/constants';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
const getLogsWithParams = async (config: any) => { const getLogsWithParams = async (config) => {
const { older_than, filter, ...values } = config; const { older_than, filter, ...values } = config;
const rawLogs = await apiClient.getQueryLog({ const rawLogs = await apiClient.getQueryLog({
...filter, ...filter,
@@ -27,20 +28,20 @@ export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUES
export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE'); export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');
export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS'); export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS');
const shortPollQueryLogs = async (data: any, filter: any, dispatch: any, getState: any, total?: any) => { const shortPollQueryLogs = async (data, filter, dispatch, getState, total) => {
const { logs, oldest } = data; const { logs, oldest } = data;
const totalData = total || { logs }; const totalData = total || { logs };
const queryForm = getState().form[FORM_NAME.LOGS_FILTER]; const queryForm = getState().form[FORM_NAME.LOGS_FILTER];
const currentQuery = queryForm && queryForm.values.search; const currentQuery = queryForm && queryForm.values.search;
const previousQuery = filter?.search; const previousQuery = filter?.search;
const isQueryTheSame = const isQueryTheSame = typeof previousQuery === 'string'
typeof previousQuery === 'string' && typeof currentQuery === 'string' && previousQuery === currentQuery; && typeof currentQuery === 'string'
&& previousQuery === currentQuery;
const isShortPollingNeeded = const isShortPollingNeeded = (logs.length < QUERY_LOGS_PAGE_LIMIT
(logs.length < QUERY_LOGS_PAGE_LIMIT || totalData.logs.length < QUERY_LOGS_PAGE_LIMIT) && || totalData.logs.length < QUERY_LOGS_PAGE_LIMIT)
oldest !== '' && && oldest !== '' && isQueryTheSame;
isQueryTheSame;
if (isShortPollingNeeded) { if (isShortPollingNeeded) {
dispatch(getAdditionalLogsRequest()); dispatch(getAdditionalLogsRequest());
@@ -74,24 +75,22 @@ export const getLogsRequest = createAction('GET_LOGS_REQUEST');
export const getLogsFailure = createAction('GET_LOGS_FAILURE'); export const getLogsFailure = createAction('GET_LOGS_FAILURE');
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS'); export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
export const updateLogs = () => async (dispatch: any, getState: any) => { export const updateLogs = () => async (dispatch, getState) => {
try { try {
const { logs, oldest, older_than } = getState().queryLogs; const { logs, oldest, older_than } = getState().queryLogs;
dispatch( dispatch(getLogsSuccess({
getLogsSuccess({ logs,
logs, oldest,
oldest, older_than,
older_than, }));
}),
);
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(getLogsFailure(error)); dispatch(getLogsFailure(error));
} }
}; };
export const getLogs = () => async (dispatch: any, getState: any) => { export const getLogs = () => async (dispatch, getState) => {
dispatch(getLogsRequest()); dispatch(getLogsRequest());
try { try {
const { isFiltered, filter, oldest } = getState().queryLogs; const { isFiltered, filter, oldest } = getState().queryLogs;
@@ -122,29 +121,26 @@ export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
* @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object * @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object
* @returns function * @returns function
*/ */
export const setLogsFilter = (filter: any) => setLogsFilterRequest(filter); export const setLogsFilter = (filter) => setLogsFilterRequest(filter);
export const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST'); export const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST');
export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE'); export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');
export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS'); export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');
export const setFilteredLogs = (filter?: any) => async (dispatch: any, getState: any) => { export const setFilteredLogs = (filter) => async (dispatch, getState) => {
dispatch(setFilteredLogsRequest()); dispatch(setFilteredLogsRequest());
try { try {
const data = await getLogsWithParams({ const data = await getLogsWithParams({
older_than: '', older_than: '',
filter, filter,
}); });
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState); const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data; const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
dispatch( dispatch(setFilteredLogsSuccess({
setFilteredLogsSuccess({ ...updatedData,
...updatedData, filter,
filter, }));
}),
);
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(setFilteredLogsFailure(error)); dispatch(setFilteredLogsFailure(error));
@@ -153,7 +149,7 @@ export const setFilteredLogs = (filter?: any) => async (dispatch: any, getState:
export const resetFilteredLogs = () => setFilteredLogs(DEFAULT_LOGS_FILTER); export const resetFilteredLogs = () => setFilteredLogs(DEFAULT_LOGS_FILTER);
export const refreshFilteredLogs = () => async (dispatch: any, getState: any) => { export const refreshFilteredLogs = () => async (dispatch, getState) => {
const { filter } = getState().queryLogs; const { filter } = getState().queryLogs;
await dispatch(setFilteredLogs(filter)); await dispatch(setFilteredLogs(filter));
}; };
@@ -162,7 +158,7 @@ export const clearLogsRequest = createAction('CLEAR_LOGS_REQUEST');
export const clearLogsFailure = createAction('CLEAR_LOGS_FAILURE'); export const clearLogsFailure = createAction('CLEAR_LOGS_FAILURE');
export const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS'); export const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS');
export const clearLogs = () => async (dispatch: any) => { export const clearLogs = () => async (dispatch) => {
dispatch(clearLogsRequest()); dispatch(clearLogsRequest());
try { try {
await apiClient.clearQueryLog(); await apiClient.clearQueryLog();
@@ -178,7 +174,7 @@ export const getLogsConfigRequest = createAction('GET_LOGS_CONFIG_REQUEST');
export const getLogsConfigFailure = createAction('GET_LOGS_CONFIG_FAILURE'); export const getLogsConfigFailure = createAction('GET_LOGS_CONFIG_FAILURE');
export const getLogsConfigSuccess = createAction('GET_LOGS_CONFIG_SUCCESS'); export const getLogsConfigSuccess = createAction('GET_LOGS_CONFIG_SUCCESS');
export const getLogsConfig = () => async (dispatch: any) => { export const getLogsConfig = () => async (dispatch) => {
dispatch(getLogsConfigRequest()); dispatch(getLogsConfigRequest());
try { try {
const data = await apiClient.getQueryLogConfig(); const data = await apiClient.getQueryLogConfig();
@@ -193,7 +189,7 @@ export const setLogsConfigRequest = createAction('SET_LOGS_CONFIG_REQUEST');
export const setLogsConfigFailure = createAction('SET_LOGS_CONFIG_FAILURE'); export const setLogsConfigFailure = createAction('SET_LOGS_CONFIG_FAILURE');
export const setLogsConfigSuccess = createAction('SET_LOGS_CONFIG_SUCCESS'); export const setLogsConfigSuccess = createAction('SET_LOGS_CONFIG_SUCCESS');
export const setLogsConfig = (config: any) => async (dispatch: any) => { export const setLogsConfig = (config) => async (dispatch) => {
dispatch(setLogsConfigRequest()); dispatch(setLogsConfigRequest());
try { try {
await apiClient.setQueryLogConfig(config); await apiClient.setQueryLogConfig(config);

View File

@@ -9,7 +9,7 @@ export const getRewritesListRequest = createAction('GET_REWRITES_LIST_REQUEST');
export const getRewritesListFailure = createAction('GET_REWRITES_LIST_FAILURE'); export const getRewritesListFailure = createAction('GET_REWRITES_LIST_FAILURE');
export const getRewritesListSuccess = createAction('GET_REWRITES_LIST_SUCCESS'); export const getRewritesListSuccess = createAction('GET_REWRITES_LIST_SUCCESS');
export const getRewritesList = () => async (dispatch: any) => { export const getRewritesList = () => async (dispatch) => {
dispatch(getRewritesListRequest()); dispatch(getRewritesListRequest());
try { try {
const data = await apiClient.getRewritesList(); const data = await apiClient.getRewritesList();
@@ -24,7 +24,7 @@ export const addRewriteRequest = createAction('ADD_REWRITE_REQUEST');
export const addRewriteFailure = createAction('ADD_REWRITE_FAILURE'); export const addRewriteFailure = createAction('ADD_REWRITE_FAILURE');
export const addRewriteSuccess = createAction('ADD_REWRITE_SUCCESS'); export const addRewriteSuccess = createAction('ADD_REWRITE_SUCCESS');
export const addRewrite = (config: any) => async (dispatch: any) => { export const addRewrite = (config) => async (dispatch) => {
dispatch(addRewriteRequest()); dispatch(addRewriteRequest());
try { try {
await apiClient.addRewrite(config); await apiClient.addRewrite(config);
@@ -47,7 +47,7 @@ export const updateRewriteSuccess = createAction('UPDATE_REWRITE_SUCCESS');
* @param {string} config.target - current DNS rewrite value * @param {string} config.target - current DNS rewrite value
* @param {string} config.update - updated DNS rewrite value * @param {string} config.update - updated DNS rewrite value
*/ */
export const updateRewrite = (config: any) => async (dispatch: any) => { export const updateRewrite = (config) => async (dispatch) => {
dispatch(updateRewriteRequest()); dispatch(updateRewriteRequest());
try { try {
await apiClient.updateRewrite(config); await apiClient.updateRewrite(config);
@@ -65,7 +65,7 @@ export const deleteRewriteRequest = createAction('DELETE_REWRITE_REQUEST');
export const deleteRewriteFailure = createAction('DELETE_REWRITE_FAILURE'); export const deleteRewriteFailure = createAction('DELETE_REWRITE_FAILURE');
export const deleteRewriteSuccess = createAction('DELETE_REWRITE_SUCCESS'); export const deleteRewriteSuccess = createAction('DELETE_REWRITE_SUCCESS');
export const deleteRewrite = (config: any) => async (dispatch: any) => { export const deleteRewrite = (config) => async (dispatch) => {
dispatch(deleteRewriteRequest()); dispatch(deleteRewriteRequest());
try { try {
await apiClient.deleteRewrite(config); await apiClient.deleteRewrite(config);

View File

@@ -6,7 +6,7 @@ export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQU
export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE'); export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');
export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS'); export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');
export const getBlockedServices = () => async (dispatch: any) => { export const getBlockedServices = () => async (dispatch) => {
dispatch(getBlockedServicesRequest()); dispatch(getBlockedServicesRequest());
try { try {
const data = await apiClient.getBlockedServices(); const data = await apiClient.getBlockedServices();
@@ -21,7 +21,7 @@ export const getAllBlockedServicesRequest = createAction('GET_ALL_BLOCKED_SERVIC
export const getAllBlockedServicesFailure = createAction('GET_ALL_BLOCKED_SERVICES_FAILURE'); export const getAllBlockedServicesFailure = createAction('GET_ALL_BLOCKED_SERVICES_FAILURE');
export const getAllBlockedServicesSuccess = createAction('GET_ALL_BLOCKED_SERVICES_SUCCESS'); export const getAllBlockedServicesSuccess = createAction('GET_ALL_BLOCKED_SERVICES_SUCCESS');
export const getAllBlockedServices = () => async (dispatch: any) => { export const getAllBlockedServices = () => async (dispatch) => {
dispatch(getAllBlockedServicesRequest()); dispatch(getAllBlockedServicesRequest());
try { try {
const data = await apiClient.getAllBlockedServices(); const data = await apiClient.getAllBlockedServices();
@@ -36,7 +36,7 @@ export const updateBlockedServicesRequest = createAction('UPDATE_BLOCKED_SERVICE
export const updateBlockedServicesFailure = createAction('UPDATE_BLOCKED_SERVICES_FAILURE'); export const updateBlockedServicesFailure = createAction('UPDATE_BLOCKED_SERVICES_FAILURE');
export const updateBlockedServicesSuccess = createAction('UPDATE_BLOCKED_SERVICES_SUCCESS'); export const updateBlockedServicesSuccess = createAction('UPDATE_BLOCKED_SERVICES_SUCCESS');
export const updateBlockedServices = (values: any) => async (dispatch: any) => { export const updateBlockedServices = (values) => async (dispatch) => {
dispatch(updateBlockedServicesRequest()); dispatch(updateBlockedServicesRequest());
try { try {
await apiClient.updateBlockedServices(values); await apiClient.updateBlockedServices(values);

View File

@@ -1,14 +1,16 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { normalizeTopStats, secondsToMilliseconds, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers'; import {
normalizeTopStats, secondsToMilliseconds, getParamsForClientsSearch, addClientInfo,
} from '../helpers/helpers';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
export const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST'); export const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST');
export const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE'); export const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE');
export const getStatsConfigSuccess = createAction('GET_STATS_CONFIG_SUCCESS'); export const getStatsConfigSuccess = createAction('GET_STATS_CONFIG_SUCCESS');
export const getStatsConfig = () => async (dispatch: any) => { export const getStatsConfig = () => async (dispatch) => {
dispatch(getStatsConfigRequest()); dispatch(getStatsConfigRequest());
try { try {
const data = await apiClient.getStatsConfig(); const data = await apiClient.getStatsConfig();
@@ -23,7 +25,7 @@ export const setStatsConfigRequest = createAction('SET_STATS_CONFIG_REQUEST');
export const setStatsConfigFailure = createAction('SET_STATS_CONFIG_FAILURE'); export const setStatsConfigFailure = createAction('SET_STATS_CONFIG_FAILURE');
export const setStatsConfigSuccess = createAction('SET_STATS_CONFIG_SUCCESS'); export const setStatsConfigSuccess = createAction('SET_STATS_CONFIG_SUCCESS');
export const setStatsConfig = (config: any) => async (dispatch: any) => { export const setStatsConfig = (config) => async (dispatch) => {
dispatch(setStatsConfigRequest()); dispatch(setStatsConfigRequest());
try { try {
await apiClient.setStatsConfig(config); await apiClient.setStatsConfig(config);
@@ -39,12 +41,11 @@ export const getStatsRequest = createAction('GET_STATS_REQUEST');
export const getStatsFailure = createAction('GET_STATS_FAILURE'); export const getStatsFailure = createAction('GET_STATS_FAILURE');
export const getStatsSuccess = createAction('GET_STATS_SUCCESS'); export const getStatsSuccess = createAction('GET_STATS_SUCCESS');
export const getStats = () => async (dispatch: any) => { export const getStats = () => async (dispatch) => {
dispatch(getStatsRequest()); dispatch(getStatsRequest());
try { try {
const stats = await apiClient.getStats(); const stats = await apiClient.getStats();
const normalizedTopClients = normalizeTopStats(stats.top_clients); const normalizedTopClients = normalizeTopStats(stats.top_clients);
const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name'); const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name');
const clients = await apiClient.findClients(clientsParams); const clients = await apiClient.findClients(clientsParams);
const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name'); const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name');
@@ -70,7 +71,7 @@ export const resetStatsRequest = createAction('RESET_STATS_REQUEST');
export const resetStatsFailure = createAction('RESET_STATS_FAILURE'); export const resetStatsFailure = createAction('RESET_STATS_FAILURE');
export const resetStatsSuccess = createAction('RESET_STATS_SUCCESS'); export const resetStatsSuccess = createAction('RESET_STATS_SUCCESS');
export const resetStats = () => async (dispatch: any) => { export const resetStats = () => async (dispatch) => {
dispatch(getStatsRequest()); dispatch(getStatsRequest());
try { try {
await apiClient.resetStats(); await apiClient.resetStats();

View File

@@ -1,16 +1,17 @@
import axios from 'axios'; import axios from 'axios';
import { BASE_URL } from '../../constants';
import { getPathWithQueryString } from '../helpers/helpers'; import { getPathWithQueryString } from '../helpers/helpers';
import { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES } from '../helpers/constants'; import {
QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES,
} from '../helpers/constants';
import { BASE_URL } from '../../constants';
import i18n from '../i18n'; import i18n from '../i18n';
import { LANGUAGES } from '../helpers/twosky'; import { LANGUAGES } from '../helpers/twosky';
class Api { class Api {
baseUrl = BASE_URL; baseUrl = BASE_URL;
async makeRequest(path: any, method = 'POST', config: any = {}) { async makeRequest(path, method = 'POST', config) {
const url = `${this.baseUrl}/${path}`; const url = `${this.baseUrl}/${path}`;
const axiosConfig = config || {}; const axiosConfig = config || {};
@@ -28,26 +29,26 @@ class Api {
return response.data; return response.data;
} catch (error) { } catch (error) {
const errorPath = url; const errorPath = url;
if (error.response) { if (error.response) {
const { pathname } = document.location; const { pathname } = document.location;
const shouldRedirect = pathname !== HTML_PAGES.LOGIN && pathname !== HTML_PAGES.INSTALL; const shouldRedirect = pathname !== HTML_PAGES.LOGIN
&& pathname !== HTML_PAGES.INSTALL;
if (error.response.status === 403 && shouldRedirect) { if (error.response.status === 403 && shouldRedirect) {
const loginPageUrl = window.location.href.replace(R_PATH_LAST_PART, HTML_PAGES.LOGIN); const loginPageUrl = window.location.href
.replace(R_PATH_LAST_PART, HTML_PAGES.LOGIN);
window.location.replace(loginPageUrl); window.location.replace(loginPageUrl);
return false; return false;
} }
throw new Error(`${errorPath} | ${error.response.data} | ${error.response.status}`); throw new Error(`${errorPath} | ${error.response.data} | ${error.response.status}`);
} }
throw new Error(`${errorPath} | ${error.message || error}`); throw new Error(`${errorPath} | ${error.message || error}`);
} }
} }
// Global methods // Global methods
GLOBAL_STATUS = { path: 'status', method: 'GET' }; GLOBAL_STATUS = { path: 'status', method: 'GET' }
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' }; GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
@@ -57,11 +58,10 @@ class Api {
getGlobalStatus() { getGlobalStatus() {
const { path, method } = this.GLOBAL_STATUS; const { path, method } = this.GLOBAL_STATUS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
testUpstream(servers: any) { testUpstream(servers) {
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS; const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
const config = { const config = {
data: servers, data: servers,
@@ -69,7 +69,7 @@ class Api {
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }
getGlobalVersion(data: any) { getGlobalVersion(data) {
const { path, method } = this.GLOBAL_VERSION; const { path, method } = this.GLOBAL_VERSION;
const config = { const config = {
data, data,
@@ -79,7 +79,6 @@ class Api {
getUpdate() { getUpdate() {
const { path, method } = this.GLOBAL_UPDATE; const { path, method } = this.GLOBAL_UPDATE;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
@@ -102,11 +101,10 @@ class Api {
getFilteringStatus() { getFilteringStatus() {
const { path, method } = this.FILTERING_STATUS; const { path, method } = this.FILTERING_STATUS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
refreshFilters(config: any) { refreshFilters(config) {
const { path, method } = this.FILTERING_REFRESH; const { path, method } = this.FILTERING_REFRESH;
const parameters = { const parameters = {
data: config, data: config,
@@ -115,7 +113,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
addFilter(config: any) { addFilter(config) {
const { path, method } = this.FILTERING_ADD_FILTER; const { path, method } = this.FILTERING_ADD_FILTER;
const parameters = { const parameters = {
data: config, data: config,
@@ -124,7 +122,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
removeFilter(config: any) { removeFilter(config) {
const { path, method } = this.FILTERING_REMOVE_FILTER; const { path, method } = this.FILTERING_REMOVE_FILTER;
const parameters = { const parameters = {
data: config, data: config,
@@ -133,7 +131,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
setRules(rules: any) { setRules(rules) {
const { path, method } = this.FILTERING_SET_RULES; const { path, method } = this.FILTERING_SET_RULES;
const parameters = { const parameters = {
data: rules, data: rules,
@@ -141,7 +139,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
setFiltersConfig(config: any) { setFiltersConfig(config) {
const { path, method } = this.FILTERING_CONFIG; const { path, method } = this.FILTERING_CONFIG;
const parameters = { const parameters = {
data: config, data: config,
@@ -149,7 +147,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
setFilterUrl(config: any) { setFilterUrl(config) {
const { path, method } = this.FILTERING_SET_URL; const { path, method } = this.FILTERING_SET_URL;
const parameters = { const parameters = {
data: config, data: config,
@@ -157,10 +155,9 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
checkHost(params: any) { checkHost(params) {
const { path, method } = this.FILTERING_CHECK_HOST; const { path, method } = this.FILTERING_CHECK_HOST;
const url = getPathWithQueryString(path, params); const url = getPathWithQueryString(path, params);
return this.makeRequest(url, method); return this.makeRequest(url, method);
} }
@@ -173,19 +170,16 @@ class Api {
getParentalStatus() { getParentalStatus() {
const { path, method } = this.PARENTAL_STATUS; const { path, method } = this.PARENTAL_STATUS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
enableParentalControl() { enableParentalControl() {
const { path, method } = this.PARENTAL_ENABLE; const { path, method } = this.PARENTAL_ENABLE;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
disableParentalControl() { disableParentalControl() {
const { path, method } = this.PARENTAL_DISABLE; const { path, method } = this.PARENTAL_DISABLE;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
@@ -198,19 +192,16 @@ class Api {
getSafebrowsingStatus() { getSafebrowsingStatus() {
const { path, method } = this.SAFEBROWSING_STATUS; const { path, method } = this.SAFEBROWSING_STATUS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
enableSafebrowsing() { enableSafebrowsing() {
const { path, method } = this.SAFEBROWSING_ENABLE; const { path, method } = this.SAFEBROWSING_ENABLE;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
disableSafebrowsing() { disableSafebrowsing() {
const { path, method } = this.SAFEBROWSING_DISABLE; const { path, method } = this.SAFEBROWSING_DISABLE;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
@@ -221,7 +212,6 @@ class Api {
getSafesearchStatus() { getSafesearchStatus() {
const { path, method } = this.SAFESEARCH_STATUS; const { path, method } = this.SAFESEARCH_STATUS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
@@ -238,7 +228,7 @@ class Api {
* @param {*} data - SafeSearchConfig * @param {*} data - SafeSearchConfig
* @returns 200 ok * @returns 200 ok
*/ */
updateSafesearch(data: any) { updateSafesearch(data) {
const { path, method } = this.SAFESEARCH_UPDATE; const { path, method } = this.SAFESEARCH_UPDATE;
return this.makeRequest(path, method, { data }); return this.makeRequest(path, method, { data });
} }
@@ -255,7 +245,7 @@ class Api {
// Language // Language
async changeLanguage(config: any) { async changeLanguage(config) {
const profile = await this.getProfile(); const profile = await this.getProfile();
profile.language = config.language; profile.language = config.language;
@@ -264,7 +254,7 @@ class Api {
// Theme // Theme
async changeTheme(config: any) { async changeTheme(config) {
const profile = await this.getProfile(); const profile = await this.getProfile();
profile.theme = config.theme; profile.theme = config.theme;
@@ -292,17 +282,15 @@ class Api {
getDhcpStatus() { getDhcpStatus() {
const { path, method } = this.DHCP_STATUS; const { path, method } = this.DHCP_STATUS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
getDhcpInterfaces() { getDhcpInterfaces() {
const { path, method } = this.DHCP_INTERFACES; const { path, method } = this.DHCP_INTERFACES;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
setDhcpConfig(config: any) { setDhcpConfig(config) {
const { path, method } = this.DHCP_SET_CONFIG; const { path, method } = this.DHCP_SET_CONFIG;
const parameters = { const parameters = {
data: config, data: config,
@@ -310,7 +298,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
findActiveDhcp(req: any) { findActiveDhcp(req) {
const { path, method } = this.DHCP_FIND_ACTIVE; const { path, method } = this.DHCP_FIND_ACTIVE;
const parameters = { const parameters = {
data: req, data: req,
@@ -318,7 +306,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
addStaticLease(config: any) { addStaticLease(config) {
const { path, method } = this.DHCP_ADD_STATIC_LEASE; const { path, method } = this.DHCP_ADD_STATIC_LEASE;
const parameters = { const parameters = {
data: config, data: config,
@@ -326,7 +314,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
removeStaticLease(config: any) { removeStaticLease(config) {
const { path, method } = this.DHCP_REMOVE_STATIC_LEASE; const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
const parameters = { const parameters = {
data: config, data: config,
@@ -334,7 +322,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
updateStaticLease(config: any) { updateStaticLease(config) {
const { path, method } = this.DHCP_UPDATE_STATIC_LEASE; const { path, method } = this.DHCP_UPDATE_STATIC_LEASE;
const parameters = { const parameters = {
data: config, data: config,
@@ -344,13 +332,11 @@ class Api {
resetDhcp() { resetDhcp() {
const { path, method } = this.DHCP_RESET; const { path, method } = this.DHCP_RESET;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
resetDhcpLeases() { resetDhcpLeases() {
const { path, method } = this.DHCP_LEASES_RESET; const { path, method } = this.DHCP_LEASES_RESET;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
@@ -363,11 +349,10 @@ class Api {
getDefaultAddresses() { getDefaultAddresses() {
const { path, method } = this.INSTALL_GET_ADDRESSES; const { path, method } = this.INSTALL_GET_ADDRESSES;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
setAllSettings(config: any) { setAllSettings(config) {
const { path, method } = this.INSTALL_CONFIGURE; const { path, method } = this.INSTALL_CONFIGURE;
const parameters = { const parameters = {
data: config, data: config,
@@ -375,7 +360,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
checkConfig(config: any) { checkConfig(config) {
const { path, method } = this.INSTALL_CHECK_CONFIG; const { path, method } = this.INSTALL_CHECK_CONFIG;
const parameters = { const parameters = {
data: config, data: config,
@@ -392,11 +377,10 @@ class Api {
getTlsStatus() { getTlsStatus() {
const { path, method } = this.TLS_STATUS; const { path, method } = this.TLS_STATUS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
setTlsConfig(config: any) { setTlsConfig(config) {
const { path, method } = this.TLS_CONFIG; const { path, method } = this.TLS_CONFIG;
const parameters = { const parameters = {
data: config, data: config,
@@ -404,7 +388,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
validateTlsConfig(config: any) { validateTlsConfig(config) {
const { path, method } = this.TLS_VALIDATE; const { path, method } = this.TLS_VALIDATE;
const parameters = { const parameters = {
data: config, data: config,
@@ -425,11 +409,10 @@ class Api {
getClients() { getClients() {
const { path, method } = this.GET_CLIENTS; const { path, method } = this.GET_CLIENTS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
addClient(config: any) { addClient(config) {
const { path, method } = this.ADD_CLIENT; const { path, method } = this.ADD_CLIENT;
const parameters = { const parameters = {
data: config, data: config,
@@ -437,7 +420,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
deleteClient(config: any) { deleteClient(config) {
const { path, method } = this.DELETE_CLIENT; const { path, method } = this.DELETE_CLIENT;
const parameters = { const parameters = {
data: config, data: config,
@@ -445,7 +428,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
updateClient(config: any) { updateClient(config) {
const { path, method } = this.UPDATE_CLIENT; const { path, method } = this.UPDATE_CLIENT;
const parameters = { const parameters = {
data: config, data: config,
@@ -453,10 +436,9 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
findClients(params: any) { findClients(params) {
const { path, method } = this.FIND_CLIENTS; const { path, method } = this.FIND_CLIENTS;
const url = getPathWithQueryString(path, params); const url = getPathWithQueryString(path, params);
return this.makeRequest(url, method); return this.makeRequest(url, method);
} }
@@ -467,11 +449,10 @@ class Api {
getAccessList() { getAccessList() {
const { path, method } = this.ACCESS_LIST; const { path, method } = this.ACCESS_LIST;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
setAccessList(config: any) { setAccessList(config) {
const { path, method } = this.ACCESS_SET; const { path, method } = this.ACCESS_SET;
const parameters = { const parameters = {
data: config, data: config,
@@ -490,11 +471,10 @@ class Api {
getRewritesList() { getRewritesList() {
const { path, method } = this.REWRITES_LIST; const { path, method } = this.REWRITES_LIST;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
addRewrite(config: any) { addRewrite(config) {
const { path, method } = this.REWRITE_ADD; const { path, method } = this.REWRITE_ADD;
const parameters = { const parameters = {
data: config, data: config,
@@ -502,7 +482,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
updateRewrite(config: any) { updateRewrite(config) {
const { path, method } = this.REWRITE_UPDATE; const { path, method } = this.REWRITE_UPDATE;
const parameters = { const parameters = {
data: config, data: config,
@@ -510,7 +490,7 @@ class Api {
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
deleteRewrite(config: any) { deleteRewrite(config) {
const { path, method } = this.REWRITE_DELETE; const { path, method } = this.REWRITE_DELETE;
const parameters = { const parameters = {
data: config, data: config,
@@ -527,17 +507,15 @@ class Api {
getAllBlockedServices() { getAllBlockedServices() {
const { path, method } = this.BLOCKED_SERVICES_ALL; const { path, method } = this.BLOCKED_SERVICES_ALL;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
getBlockedServices() { getBlockedServices() {
const { path, method } = this.BLOCKED_SERVICES_GET; const { path, method } = this.BLOCKED_SERVICES_GET;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
updateBlockedServices(config: any) { updateBlockedServices(config) {
const { path, method } = this.BLOCKED_SERVICES_UPDATE; const { path, method } = this.BLOCKED_SERVICES_UPDATE;
const parameters = { const parameters = {
data: config, data: config,
@@ -556,17 +534,15 @@ class Api {
getStats() { getStats() {
const { path, method } = this.GET_STATS; const { path, method } = this.GET_STATS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
getStatsConfig() { getStatsConfig() {
const { path, method } = this.GET_STATS_CONFIG; const { path, method } = this.GET_STATS_CONFIG;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
setStatsConfig(data: any) { setStatsConfig(data) {
const { path, method } = this.UPDATE_STATS_CONFIG; const { path, method } = this.UPDATE_STATS_CONFIG;
const config = { const config = {
data, data,
@@ -576,7 +552,6 @@ class Api {
resetStats() { resetStats() {
const { path, method } = this.STATS_RESET; const { path, method } = this.STATS_RESET;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
@@ -589,22 +564,20 @@ class Api {
QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' }; QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' };
getQueryLog(params: any) { getQueryLog(params) {
const { path, method } = this.GET_QUERY_LOG; const { path, method } = this.GET_QUERY_LOG;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
params.limit = QUERY_LOGS_PAGE_LIMIT; params.limit = QUERY_LOGS_PAGE_LIMIT;
const url = getPathWithQueryString(path, params); const url = getPathWithQueryString(path, params);
return this.makeRequest(url, method); return this.makeRequest(url, method);
} }
getQueryLogConfig() { getQueryLogConfig() {
const { path, method } = this.GET_QUERY_LOG_CONFIG; const { path, method } = this.GET_QUERY_LOG_CONFIG;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
setQueryLogConfig(data: any) { setQueryLogConfig(data) {
const { path, method } = this.UPDATE_QUERY_LOG_CONFIG; const { path, method } = this.UPDATE_QUERY_LOG_CONFIG;
const config = { const config = {
data, data,
@@ -614,14 +587,13 @@ class Api {
clearQueryLog() { clearQueryLog() {
const { path, method } = this.QUERY_LOG_CLEAR; const { path, method } = this.QUERY_LOG_CLEAR;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
// Login // Login
LOGIN = { path: 'login', method: 'POST' }; LOGIN = { path: 'login', method: 'POST' };
login(data: any) { login(data) {
const { path, method } = this.LOGIN; const { path, method } = this.LOGIN;
const config = { const config = {
data, data,
@@ -636,11 +608,10 @@ class Api {
getProfile() { getProfile() {
const { path, method } = this.GET_PROFILE; const { path, method } = this.GET_PROFILE;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
setProfile(data: any) { setProfile(data) {
const theme = data.theme ? data.theme : THEMES.auto; const theme = data.theme ? data.theme : THEMES.auto;
const defaultLanguage = i18n.language ? i18n.language : LANGUAGES.en; const defaultLanguage = i18n.language ? i18n.language : LANGUAGES.en;
const language = data.language ? data.language : defaultLanguage; const language = data.language ? data.language : defaultLanguage;
@@ -658,11 +629,10 @@ class Api {
getDnsConfig() { getDnsConfig() {
const { path, method } = this.GET_DNS_CONFIG; const { path, method } = this.GET_DNS_CONFIG;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
setDnsConfig(data: any) { setDnsConfig(data) {
const { path, method } = this.SET_DNS_CONFIG; const { path, method } = this.SET_DNS_CONFIG;
const config = { const config = {
data, data,
@@ -672,7 +642,7 @@ class Api {
SET_PROTECTION = { path: 'protection', method: 'POST' }; SET_PROTECTION = { path: 'protection', method: 'POST' };
setProtection(data: any) { setProtection(data) {
const { enabled, duration } = data; const { enabled, duration } = data;
const { path, method } = this.SET_PROTECTION; const { path, method } = this.SET_PROTECTION;
@@ -684,7 +654,6 @@ class Api {
clearCache() { clearCache() {
const { path, method } = this.CLEAR_CACHE; const { path, method } = this.CLEAR_CACHE;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
} }

View File

@@ -15,8 +15,8 @@
--btn-success-bgcolor: #5eba00; --btn-success-bgcolor: #5eba00;
--form-disabled-bgcolor: #f8f9fa; --form-disabled-bgcolor: #f8f9fa;
--form-disabled-color: #495057; --form-disabled-color: #495057;
--rt-nodata-bgcolor: rgba(255, 255, 255, 0.8); --rt-nodata-bgcolor: rgba(255,255,255,0.8);
--rt-nodata-color: rgba(0, 0, 0, 0.5); --rt-nodata-color: rgba(0,0,0,0.5);
--modal-overlay-bgcolor: rgba(255, 255, 255, 0.75); --modal-overlay-bgcolor: rgba(255, 255, 255, 0.75);
--logs__table-bgcolor: #fff; --logs__table-bgcolor: #fff;
--logs__row--blue-bgcolor: #e5effd; --logs__row--blue-bgcolor: #e5effd;
@@ -28,7 +28,7 @@
--gray-d8: #d8d8d8; --gray-d8: #d8d8d8;
--gray-f3: #f3f3f3; --gray-f3: #f3f3f3;
--loading-bg: rgba(255, 255, 255, 0.48); --loading-bg: rgba(255, 255, 255, 0.48);
--font-family-monospace: Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace; --font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
--font-size-disable-autozoom: 1rem; --font-size-disable-autozoom: 1rem;
--alert-message-color: #24426c; --alert-message-color: #24426c;
--alert-message-border: #cbdbf2; --alert-message-border: #cbdbf2;
@@ -37,7 +37,7 @@
--radio-bg: #ffffff; --radio-bg: #ffffff;
} }
[data-theme='dark'] { [data-theme="dark"] {
--black: #ffffff; --black: #ffffff;
--bgcolor: #131313; --bgcolor: #131313;
--mcolor: #e6e6e6; --mcolor: #e6e6e6;
@@ -74,14 +74,12 @@
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
} }
/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */ /* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
input, input, select, textarea {
select,
textarea {
font-size: var(--font-size-disable-autozoom); font-size: var(--font-size-disable-autozoom);
} }
} }

View File

@@ -1,5 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { HashRouter, Route } from 'react-router-dom'; import { HashRouter, Route } from 'react-router-dom';
import LoadingBar from 'react-redux-loading-bar'; import LoadingBar from 'react-redux-loading-bar';
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
@@ -10,6 +9,8 @@ import '../ui/ReactTable.css';
import './index.css'; import './index.css';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import propTypes from 'prop-types';
import Toasts from '../Toasts'; import Toasts from '../Toasts';
import Footer from '../ui/Footer'; import Footer from '../ui/Footer';
import Status from '../ui/Status'; import Status from '../ui/Status';
@@ -18,14 +19,15 @@ import UpdateOverlay from '../ui/UpdateOverlay';
import EncryptionTopline from '../ui/EncryptionTopline'; import EncryptionTopline from '../ui/EncryptionTopline';
import Icons from '../ui/Icons'; import Icons from '../ui/Icons';
import i18n from '../../i18n'; import i18n from '../../i18n';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS, THEMES } from '../../helpers/constants'; import {
FILTERS_URLS,
MENU_URLS,
SETTINGS_URLS,
THEMES,
} from '../../helpers/constants';
import { getLogsUrlParams, setHtmlLangAttr, setUITheme } from '../../helpers/helpers'; import { getLogsUrlParams, setHtmlLangAttr, setUITheme } from '../../helpers/helpers';
import Header from '../Header'; import Header from '../Header';
import { changeLanguage, getDnsStatus, getTimerStatus } from '../../actions'; import { changeLanguage, getDnsStatus, getTimerStatus } from '../../actions';
import Dashboard from '../../containers/Dashboard'; import Dashboard from '../../containers/Dashboard';
@@ -33,19 +35,15 @@ import SetupGuide from '../../containers/SetupGuide';
import Settings from '../../containers/Settings'; import Settings from '../../containers/Settings';
import Dns from '../../containers/Dns'; import Dns from '../../containers/Dns';
import Encryption from '../../containers/Encryption'; import Encryption from '../../containers/Encryption';
import Dhcp from '../Settings/Dhcp'; import Dhcp from '../Settings/Dhcp';
import Clients from '../../containers/Clients'; import Clients from '../../containers/Clients';
import DnsBlocklist from '../../containers/DnsBlocklist'; import DnsBlocklist from '../../containers/DnsBlocklist';
import DnsAllowlist from '../../containers/DnsAllowlist'; import DnsAllowlist from '../../containers/DnsAllowlist';
import DnsRewrites from '../../containers/DnsRewrites'; import DnsRewrites from '../../containers/DnsRewrites';
import CustomRules from '../../containers/CustomRules'; import CustomRules from '../../containers/CustomRules';
import Services from '../Filters/Services'; import Services from '../Filters/Services';
import Logs from '../Logs'; import Logs from '../Logs';
import ProtectionTimer from '../ProtectionTimer'; import ProtectionTimer from '../ProtectionTimer';
import { RootState } from '../../initialState';
const ROUTES = [ const ROUTES = [
{ {
@@ -103,17 +101,26 @@ const ROUTES = [
}, },
]; ];
const renderRoute = ({ path, component, exact }, idx) => <Route
key={idx}
exact={exact}
path={path}
component={component}
/>;
const App = () => { const App = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { language, isCoreRunning, isUpdateAvailable, processing, theme } = useSelector< const {
RootState, language,
RootState['dashboard'] isCoreRunning,
>((state) => state.dashboard, shallowEqual); isUpdateAvailable,
processing,
theme,
} = useSelector((state) => state.dashboard, shallowEqual);
const { processing: processingEncryption } = useSelector<RootState, RootState['encryption']>( const { processing: processingEncryption } = useSelector((
(state) => state.encryption, state,
shallowEqual, ) => state.encryption, shallowEqual);
);
const updateAvailable = isCoreRunning && isUpdateAvailable; const updateAvailable = isCoreRunning && isUpdateAvailable;
@@ -150,7 +157,7 @@ const App = () => {
setLanguage(); setLanguage();
}, [language]); }, [language]);
const handleAutoTheme = (e: any, accountTheme: any) => { const handleAutoTheme = (e, accountTheme) => {
if (accountTheme !== THEMES.auto) { if (accountTheme !== THEMES.auto) {
return; return;
} }
@@ -188,50 +195,35 @@ const App = () => {
window.location.reload(); window.location.reload();
}; };
return ( return <HashRouter hashType="noslash">
<HashRouter hashType="noslash"> {updateAvailable && <>
{updateAvailable && ( <UpdateTopline />
<> <UpdateOverlay />
<UpdateTopline /> </>}
{!processingEncryption && <EncryptionTopline />}
<LoadingBar className="loading-bar" updateTime={1000} />
<Header />
<ProtectionTimer />
<div className="container container--wrap pb-5 pt-5">
{processing && <Loading />}
{!isCoreRunning && <div className="row row-cards">
<div className="col-lg-12">
<Status reloadPage={reloadPage} message="dns_start" />
<Loading />
</div>
</div>}
{!processing && isCoreRunning && ROUTES.map(renderRoute)}
</div>
<Footer />
<Toasts />
<Icons />
</HashRouter>;
};
<UpdateOverlay /> renderRoute.propTypes = {
</> path: propTypes.oneOfType([propTypes.string, propTypes.arrayOf(propTypes.string)]).isRequired,
)} component: propTypes.element.isRequired,
exact: propTypes.bool,
{!processingEncryption && <EncryptionTopline />}
<LoadingBar className="loading-bar" updateTime={1000} />
<Header />
<ProtectionTimer />
<div className="container container--wrap pb-5 pt-5">
{processing && <Loading />}
{!isCoreRunning && (
<div className="row row-cards">
<div className="col-lg-12">
<Status reloadPage={reloadPage} message="dns_start" />
<Loading />
</div>
</div>
)}
{!processing &&
isCoreRunning &&
ROUTES.map((route, index) => (
<Route key={index} exact={route.exact} path={route.path} component={route.component} />
))}
</div>
<Footer />
<Toasts />
<Icons />
</HashRouter>
);
}; };
export default hot(App); export default hot(App);

View File

@@ -1,37 +1,25 @@
import React from 'react'; import React from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next'; import { withTranslation, Trans } from 'react-i18next';
import { TFunction } from 'i18next';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Cell from '../ui/Cell'; import Cell from '../ui/Cell';
import DomainCell from './DomainCell'; import DomainCell from './DomainCell';
import { getPercent } from '../../helpers/helpers'; import { getPercent } from '../../helpers/helpers';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants'; import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
const CountCell = (totalBlocked: any) => const CountCell = (totalBlocked) => function cell(row) {
function cell(row: any) { const { value } = row;
const { value } = row; const percent = getPercent(totalBlocked, value);
const percent = getPercent(totalBlocked, value);
return <Cell value={value} percent={percent} color={STATUS_COLORS.red} search={row.original.domain} />; return <Cell value={value}
}; percent={percent}
color={STATUS_COLORS.red}
interface BlockedDomainsProps { search={row.original.domain}
topBlockedDomains: unknown[]; />;
blockedFiltering: number; };
replacedSafebrowsing: number;
replacedSafesearch: number;
replacedParental: number;
refreshButton: React.ReactNode;
subtitle: string;
t: TFunction;
}
const BlockedDomains = ({ const BlockedDomains = ({
t, t,
@@ -42,13 +30,20 @@ const BlockedDomains = ({
replacedSafebrowsing, replacedSafebrowsing,
replacedParental, replacedParental,
replacedSafesearch, replacedSafesearch,
}: BlockedDomainsProps) => { }) => {
const totalBlocked = blockedFiltering + replacedSafebrowsing + replacedParental + replacedSafesearch; const totalBlocked = (
blockedFiltering + replacedSafebrowsing + replacedParental + replacedSafesearch
);
return ( return (
<Card title={t('top_blocked_domains')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}> <Card
title={t('top_blocked_domains')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<ReactTable <ReactTable
data={topBlockedDomains.map(({ name: domain, count }: any) => ({ data={topBlockedDomains.map(({ name: domain, count }) => ({
domain, domain,
count, count,
}))} }))}
@@ -75,4 +70,15 @@ const BlockedDomains = ({
); );
}; };
BlockedDomains.propTypes = {
topBlockedDomains: PropTypes.array.isRequired,
blockedFiltering: PropTypes.number.isRequired,
replacedSafebrowsing: PropTypes.number.isRequired,
replacedSafesearch: PropTypes.number.isRequired,
replacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(BlockedDomains); export default withTranslation()(BlockedDomains);

View File

@@ -1,12 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Cell from '../ui/Cell'; import Cell from '../ui/Cell';
@@ -18,14 +16,11 @@ import {
TABLES_MIN_ROWS, TABLES_MIN_ROWS,
} from '../../helpers/constants'; } from '../../helpers/constants';
import { toggleClientBlock } from '../../actions/access'; import { toggleClientBlock } from '../../actions/access';
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell'; import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
import { getStats } from '../../actions/stats'; import { getStats } from '../../actions/stats';
import IconTooltip from '../Logs/Cells/IconTooltip'; import IconTooltip from '../Logs/Cells/IconTooltip';
import { RootState } from '../../initialState';
const getClientsPercentColor = (percent: any) => { const getClientsPercentColor = (percent) => {
if (percent > 50) { if (percent > 50) {
return STATUS_COLORS.green; return STATUS_COLORS.green;
} }
@@ -35,13 +30,9 @@ const getClientsPercentColor = (percent: any) => {
return STATUS_COLORS.red; return STATUS_COLORS.red;
}; };
const CountCell = (row: any) => { const CountCell = (row) => {
const { const { value, original: { ip } } = row;
value, const numDnsQueries = useSelector((state) => state.stats.numDnsQueries, shallowEqual);
original: { ip },
} = row;
const numDnsQueries = useSelector<RootState>((state) => state.stats.numDnsQueries, shallowEqual);
const percent = getPercent(numDnsQueries, value); const percent = getPercent(numDnsQueries, value);
const percentColor = getClientsPercentColor(percent); const percentColor = getClientsPercentColor(percent);
@@ -49,29 +40,22 @@ const CountCell = (row: any) => {
return <Cell value={value} percent={percent} color={percentColor} search={ip} />; return <Cell value={value} percent={percent} color={percentColor} search={ip} />;
}; };
const renderBlockingButton = (ip: any, disallowed: any, disallowed_rule: any) => { const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const processingSet = useSelector((state) => state.access.processingSet);
const processingSet = useSelector<RootState, RootState['access']['processingSet']>( const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
(state) => state.access.processingSet,
);
const allowedClients = useSelector<RootState, RootState['access']['allowed_clients']>(
(state) => state.access.allowed_clients,
shallowEqual,
);
const [isOptionsOpened, setOptionsOpened] = useState(false); const [isOptionsOpened, setOptionsOpened] = useState(false);
const toggleClientStatus = async (ip: any, disallowed: any, disallowed_rule: any) => { const toggleClientStatus = async (ip, disallowed, disallowed_rule) => {
let confirmMessage; let confirmMessage;
if (disallowed) { if (disallowed) {
confirmMessage = t('client_confirm_unblock', { ip: disallowed_rule || ip }); confirmMessage = t('client_confirm_unblock', { ip: disallowed_rule || ip });
} else { } else {
confirmMessage = `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`; confirmMessage = `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`;
if (allowedClients.length > 0) { if (allowedСlients.length > 0) {
confirmMessage = confirmMessage.concat(`\n\n${t('filter_allowlist', { disallowed_rule })}`); confirmMessage = confirmMessage.concat(`\n\n${t('filter_allowlist', { disallowed_rule })}`);
} }
} }
@@ -89,11 +73,15 @@ const renderBlockingButton = (ip: any, disallowed: any, disallowed_rule: any) =>
const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK; const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const lastRuleInAllowlist = !disallowed && allowedClients === disallowed_rule; const lastRuleInAllowlist = !disallowed && allowedСlients === disallowed_rule;
const disabled = processingSet || lastRuleInAllowlist; const disabled = processingSet || lastRuleInAllowlist;
return ( return (
<div className="table__action"> <div className="table__action">
<button type="button" className="btn btn-icon btn-sm px-0" onClick={() => setOptionsOpened(true)}> <button
type="button"
className="btn btn-icon btn-sm px-0"
onClick={() => setOptionsOpened(true)}
>
<svg className="icon24 icon--lightgray button-action__icon"> <svg className="icon24 icon--lightgray button-action__icon">
<use xlinkHref="#bullets" /> <use xlinkHref="#bullets" />
</svg> </svg>
@@ -104,18 +92,16 @@ const renderBlockingButton = (ip: any, disallowed: any, disallowed_rule: any) =>
tooltipClass="button-action--arrow-option-container" tooltipClass="button-action--arrow-option-container"
xlinkHref="bullets" xlinkHref="bullets"
triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger" triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
content={ content={(
<button <button
className={classNames( className={classNames('button-action--arrow-option px-4 py-1', disallowed ? 'bg--green' : 'bg--danger')}
'button-action--arrow-option px-4 py-1',
disallowed ? 'bg--green' : 'bg--danger',
)}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}> title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}
>
<Trans>{text}</Trans> <Trans>{text}</Trans>
</button> </button>
} )}
placement="bottom-end" placement="bottom-end"
trigger="click" trigger="click"
onVisibilityChange={setOptionsOpened} onVisibilityChange={setOptionsOpened}
@@ -127,42 +113,35 @@ const renderBlockingButton = (ip: any, disallowed: any, disallowed_rule: any) =>
); );
}; };
const ClientCell = (row: any) => { const ClientCell = (row) => {
const { const { value, original: { info, info: { disallowed, disallowed_rule } } } = row;
value,
original: {
info,
info: { disallowed, disallowed_rule },
},
} = row;
return ( return <>
<> <div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
<div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center"> {renderFormattedClientCell(value, info, true)}
{renderFormattedClientCell(value, info, true)} {renderBlockingButton(value, disallowed, disallowed_rule)}
{renderBlockingButton(value, disallowed, disallowed_rule)} </div>
</div> </>;
</>
);
}; };
interface ClientsProps { const Clients = ({
refreshButton: React.ReactNode; refreshButton,
subtitle: string; subtitle,
} }) => {
const Clients = ({ refreshButton, subtitle }: ClientsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
const topClients = useSelector<RootState, RootState['stats']['topClients']>(
(state) => state.stats.topClients,
shallowEqual,
);
return ( return (
<Card title={t('top_clients')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}> <Card
title={t('top_clients')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<ReactTable <ReactTable
data={topClients.map(({ name: ip, count, info, blocked }: any) => ({ data={topClients.map(({
name: ip, count, info, blocked,
}) => ({
ip, ip,
count, count,
info, info,
@@ -188,14 +167,12 @@ const Clients = ({ refreshButton, subtitle }: ClientsProps) => {
minRows={TABLES_MIN_ROWS} minRows={TABLES_MIN_ROWS}
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE} defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
className="-highlight card-table-overflow--limited clients__table" className="-highlight card-table-overflow--limited clients__table"
getTrProps={(_state: any, rowInfo: any) => { getTrProps={(_state, rowInfo) => {
if (!rowInfo) { if (!rowInfo) {
return {}; return {};
} }
const { const { info: { disallowed } } = rowInfo.original;
info: { disallowed },
} = rowInfo.original;
return disallowed ? { className: 'logs__row--red' } : {}; return disallowed ? { className: 'logs__row--red' } : {};
}} }}
@@ -204,4 +181,9 @@ const Clients = ({ refreshButton, subtitle }: ClientsProps) => {
); );
}; };
Clients.propTypes = {
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
};
export default Clients; export default Clients;

View File

@@ -1,52 +1,41 @@
import React from 'react'; import React from 'react';
import propTypes from 'prop-types';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import round from 'lodash/round'; import round from 'lodash/round';
import { shallowEqual, useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import Card from '../ui/Card'; import Card from '../ui/Card';
import { formatNumber, msToDays, msToHours } from '../../helpers/helpers'; import { formatNumber, msToDays, msToHours } from '../../helpers/helpers';
import LogsSearchLink from '../ui/LogsSearchLink'; import LogsSearchLink from '../ui/LogsSearchLink';
import { RESPONSE_FILTER, TIME_UNITS } from '../../helpers/constants'; import { RESPONSE_FILTER, TIME_UNITS } from '../../helpers/constants';
import Tooltip from '../ui/Tooltip'; import Tooltip from '../ui/Tooltip';
import { RootState } from '../../initialState';
interface RowProps { const Row = ({
label: string; label, count, response_status, tooltipTitle, translationComponents,
count: string; }) => {
response_status?: string; const content = response_status
tooltipTitle: string; ? <LogsSearchLink response_status={response_status}>{formatNumber(count)}</LogsSearchLink>
translationComponents?: React.ReactElement[]; : count;
}
const Row = ({ label, count, response_status, tooltipTitle, translationComponents }: RowProps) => {
const content = response_status ? (
<LogsSearchLink response_status={response_status}>{formatNumber(count)}</LogsSearchLink>
) : (
count
);
return ( return (
<div className="counters__row" key={label}> <div className="counters__row" key={label}>
<div className="counters__column"> <div className="counters__column">
<span className="counters__title"> <span className="counters__title">
<Trans components={translationComponents}>{label}</Trans> <Trans components={translationComponents}>
{label}
</Trans>
</span> </span>
<span className="counters__tooltip"> <span className="counters__tooltip">
<Tooltip <Tooltip
content={tooltipTitle} content={tooltipTitle}
placement="top" placement="top"
className="tooltip-container tooltip-custom--narrow text-center"> className="tooltip-container tooltip-custom--narrow text-center"
>
<svg className="icons icon--20 icon--lightgray ml-2"> <svg className="icons icon--20 icon--lightgray ml-2">
<use xlinkHref="#question" /> <use xlinkHref="#question" />
</svg> </svg>
</Tooltip> </Tooltip>
</span> </span>
</div> </div>
<div className="counters__column counters__column--value"> <div className="counters__column counters__column--value">
<strong>{content}</strong> <strong>{content}</strong>
</div> </div>
@@ -54,12 +43,7 @@ const Row = ({ label, count, response_status, tooltipTitle, translationComponent
); );
}; };
interface CountersProps { const Counters = ({ refreshButton, subtitle }) => {
refreshButton: React.ReactNode;
subtitle: string;
}
const Counters = ({ refreshButton, subtitle }: CountersProps) => {
const { const {
interval, interval,
numDnsQueries, numDnsQueries,
@@ -69,67 +53,77 @@ const Counters = ({ refreshButton, subtitle }: CountersProps) => {
numReplacedSafesearch, numReplacedSafesearch,
avgProcessingTime, avgProcessingTime,
timeUnits, timeUnits,
} = useSelector<RootState, RootState['stats']>((state) => state.stats, shallowEqual); } = useSelector((state) => state.stats, shallowEqual);
const { t } = useTranslation(); const { t } = useTranslation();
const dnsQueryTooltip = const dnsQueryTooltip = timeUnits === TIME_UNITS.HOURS
timeUnits === TIME_UNITS.HOURS ? t('number_of_dns_query_hours', { count: msToHours(interval) })
? t('number_of_dns_query_hours', { count: msToHours(interval) }) : t('number_of_dns_query_days', { count: msToDays(interval) });
: t('number_of_dns_query_days', { count: msToDays(interval) });
const rows = [ const rows = [
{ {
label: 'dns_query', label: 'dns_query',
count: numDnsQueries.toString(), count: numDnsQueries,
tooltipTitle: dnsQueryTooltip, tooltipTitle: dnsQueryTooltip,
response_status: RESPONSE_FILTER.ALL.QUERY, response_status: RESPONSE_FILTER.ALL.QUERY,
}, },
{ {
label: 'blocked_by', label: 'blocked_by',
count: numBlockedFiltering.toString(), count: numBlockedFiltering,
tooltipTitle: 'number_of_dns_query_blocked_24_hours', tooltipTitle: 'number_of_dns_query_blocked_24_hours',
response_status: RESPONSE_FILTER.BLOCKED.QUERY, response_status: RESPONSE_FILTER.BLOCKED.QUERY,
translationComponents: [<a href="#filters" key="0">link</a>],
translationComponents: [
<a href="#filters" key="0">
link
</a>,
],
}, },
{ {
label: 'stats_malware_phishing', label: 'stats_malware_phishing',
count: numReplacedSafebrowsing.toString(), count: numReplacedSafebrowsing,
tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec', tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec',
response_status: RESPONSE_FILTER.BLOCKED_THREATS.QUERY, response_status: RESPONSE_FILTER.BLOCKED_THREATS.QUERY,
}, },
{ {
label: 'stats_adult', label: 'stats_adult',
count: numReplacedParental.toString(), count: numReplacedParental,
tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult', tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult',
response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY, response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY,
}, },
{ {
label: 'enforced_save_search', label: 'enforced_save_search',
count: numReplacedSafesearch.toString(), count: numReplacedSafesearch,
tooltipTitle: 'number_of_dns_query_to_safe_search', tooltipTitle: 'number_of_dns_query_to_safe_search',
response_status: RESPONSE_FILTER.SAFE_SEARCH.QUERY, response_status: RESPONSE_FILTER.SAFE_SEARCH.QUERY,
}, },
{ {
label: 'average_processing_time', label: 'average_processing_time',
count: avgProcessingTime ? `${round(avgProcessingTime)} ms` : '0', count: avgProcessingTime ? `${round(avgProcessingTime)} ms` : 0,
tooltipTitle: 'average_processing_time_hint', tooltipTitle: 'average_processing_time_hint',
}, },
]; ];
return ( return (
<Card title={t('general_statistics')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}> <Card
title={t('general_statistics')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<div className="counters"> <div className="counters">
{rows.map((row, index) => { {rows.map(Row)}
return <Row {...row} key={index} />;
})}
</div> </div>
</Card> </Card>
); );
}; };
Row.propTypes = {
label: propTypes.string.isRequired,
count: propTypes.string.isRequired,
response_status: propTypes.string,
tooltipTitle: propTypes.string.isRequired,
translationComponents: propTypes.arrayOf(propTypes.element),
};
Counters.propTypes = {
refreshButton: propTypes.node.isRequired,
subtitle: propTypes.string.isRequired,
};
export default Counters; export default Counters;

View File

@@ -0,0 +1,77 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
import { getSourceData, getTrackerData } from '../../helpers/trackers/trackers';
import Tooltip from '../ui/Tooltip';
import { captitalizeWords } from '../../helpers/helpers';
const renderLabel = (value) => <strong><Trans>{value}</Trans></strong>;
const renderLink = ({ url, name }) => <a
className="tooltip-custom__content-link"
target="_blank"
rel="noopener noreferrer"
href={url}
>
<strong>{name}</strong>
</a>;
const getTrackerInfo = (trackerData) => [{
key: 'name_table_header',
value: trackerData,
render: renderLink,
},
{
key: 'category_label',
value: captitalizeWords(trackerData.category),
render: renderLabel,
},
{
key: 'source_label',
value: getSourceData(trackerData),
render: renderLink,
}];
const DomainCell = ({ value }) => {
const trackerData = getTrackerData(value);
const content = trackerData && <div className="popover__list">
<div className="tooltip-custom__content-title mb-1">
<Trans>found_in_known_domain_db</Trans>
</div>
{getTrackerInfo(trackerData)
.map(({ key, value, render }) => <div
key={key}
className="tooltip-custom__content-item"
>
<Trans>{key}</Trans>: {render(value)}
</div>)}
</div>;
return (
<div className="logs__row">
<div className="logs__text" title={value}>
{value}
</div>
{trackerData
&& <Tooltip content={content} placement="top"
className="tooltip-container tooltip-custom--wide">
<svg className="icons icon--24 icon--green ml-1">
<use xlinkHref="#privacy" />
</svg>
</Tooltip>}
</div>
);
};
DomainCell.propTypes = {
value: PropTypes.string.isRequired,
};
renderLink.propTypes = {
url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
};
export default DomainCell;

View File

@@ -1,81 +0,0 @@
import React from 'react';
import { Trans } from 'react-i18next';
import { getSourceData, getTrackerData } from '../../helpers/trackers/trackers';
import Tooltip from '../ui/Tooltip';
import { captitalizeWords } from '../../helpers/helpers';
const renderLabel = (value: any) => (
<strong>
<Trans>{value}</Trans>
</strong>
);
interface renderLinkProps {
url: string;
name: string;
}
const renderLink = ({ url, name }: renderLinkProps) => (
<a className="tooltip-custom__content-link" target="_blank" rel="noopener noreferrer" href={url}>
<strong>{name}</strong>
</a>
);
const getTrackerInfo = (trackerData: any) => [
{
key: 'name_table_header',
value: trackerData,
render: renderLink,
},
{
key: 'category_label',
value: captitalizeWords(trackerData.category),
render: renderLabel,
},
{
key: 'source_label',
value: getSourceData(trackerData),
render: renderLink,
},
];
interface DomainCellProps {
value: string;
}
const DomainCell = ({ value }: DomainCellProps) => {
const trackerData = getTrackerData(value);
const content = trackerData && (
<div className="popover__list">
<div className="tooltip-custom__content-title mb-1">
<Trans>found_in_known_domain_db</Trans>
</div>
{getTrackerInfo(trackerData).map(({ key, value, render }) => (
<div key={key} className="tooltip-custom__content-item">
<Trans>{key}</Trans>: {render(value)}
</div>
))}
</div>
);
return (
<div className="logs__row">
<div className="logs__text" title={value}>
{value}
</div>
{trackerData && (
<Tooltip content={content} placement="top" className="tooltip-container tooltip-custom--wide">
<svg className="icons icon--24 icon--green ml-1">
<use xlinkHref="#privacy" />
</svg>
</Tooltip>
)}
</div>
);
};
export default DomainCell;

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next'; import { withTranslation, Trans } from 'react-i18next';
import Card from '../ui/Card'; import Card from '../ui/Card';
@@ -9,10 +8,9 @@ import Cell from '../ui/Cell';
import DomainCell from './DomainCell'; import DomainCell from './DomainCell';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants'; import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
import { getPercent } from '../../helpers/helpers'; import { getPercent } from '../../helpers/helpers';
const getQueriedPercentColor = (percent: any) => { const getQueriedPercentColor = (percent) => {
if (percent > 10) { if (percent > 10) {
return STATUS_COLORS.red; return STATUS_COLORS.red;
} }
@@ -22,27 +20,26 @@ const getQueriedPercentColor = (percent: any) => {
return STATUS_COLORS.green; return STATUS_COLORS.green;
}; };
const countCell = (dnsQueries: any) => const countCell = (dnsQueries) => function cell(row) {
function cell(row: any) { const { value } = row;
const { value } = row; const percent = getPercent(dnsQueries, value);
const percent = getPercent(dnsQueries, value); const percentColor = getQueriedPercentColor(percent);
const percentColor = getQueriedPercentColor(percent);
return <Cell value={value} percent={percent} color={percentColor} search={row.original.domain} />; return <Cell value={value} percent={percent} color={percentColor}
}; search={row.original.domain} />;
};
interface QueriedDomainsProps { const QueriedDomains = ({
topQueriedDomains: unknown[]; t, refreshButton, topQueriedDomains, subtitle, dnsQueries,
dnsQueries: number; }) => (
refreshButton: React.ReactNode; <Card
subtitle: string; title={t('stats_query_domain')}
t: (...args: unknown[]) => string; subtitle={subtitle}
} bodyType="card-table"
refresh={refreshButton}
const QueriedDomains = ({ t, refreshButton, topQueriedDomains, subtitle, dnsQueries }: QueriedDomainsProps) => ( >
<Card title={t('stats_query_domain')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
<ReactTable <ReactTable
data={topQueriedDomains.map(({ name: domain, count }: any) => ({ data={topQueriedDomains.map(({ name: domain, count }) => ({
domain, domain,
count, count,
}))} }))}
@@ -68,4 +65,12 @@ const QueriedDomains = ({ t, refreshButton, topQueriedDomains, subtitle, dnsQuer
</Card> </Card>
); );
QueriedDomains.propTypes = {
topQueriedDomains: PropTypes.array.isRequired,
dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(QueriedDomains); export default withTranslation()(QueriedDomains);

View File

@@ -1,27 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { withTranslation, Trans } from 'react-i18next'; import { withTranslation, Trans } from 'react-i18next';
import StatsCard from './StatsCard'; import StatsCard from './StatsCard';
import { getPercent, normalizeHistory } from '../../helpers/helpers'; import { getPercent, normalizeHistory } from '../../helpers/helpers';
import { RESPONSE_FILTER } from '../../helpers/constants'; import { RESPONSE_FILTER } from '../../helpers/constants';
const getNormalizedHistory = (data: any, interval: any, id: any) => [{ data: normalizeHistory(data), id }]; const getNormalizedHistory = (data, interval, id) => [
{ data: normalizeHistory(data, interval), id },
interface StatisticsProps { ];
interval: number;
dnsQueries: number[];
blockedFiltering: unknown[];
replacedSafebrowsing: unknown[];
replacedParental: unknown[];
numDnsQueries: number;
numBlockedFiltering: number;
numReplacedSafebrowsing: number;
numReplacedParental: number;
refreshButton: React.ReactNode;
}
const Statistics = ({ const Statistics = ({
interval, interval,
@@ -33,68 +21,61 @@ const Statistics = ({
numBlockedFiltering, numBlockedFiltering,
numReplacedSafebrowsing, numReplacedSafebrowsing,
numReplacedParental, numReplacedParental,
}: StatisticsProps) => ( }) => (
<div className="row"> <div className="row">
<div className="col-sm-6 col-lg-3"> <div className="col-sm-6 col-lg-3">
<StatsCard <StatsCard
total={numDnsQueries} total={numDnsQueries}
lineData={getNormalizedHistory(dnsQueries, interval, 'dnsQuery')} lineData={getNormalizedHistory(dnsQueries, interval, 'dnsQuery')}
title={ title={<Link to="logs"><Trans>dns_query</Trans></Link>}
<Link to="logs">
<Trans>dns_query</Trans>
</Link>
}
color="blue" color="blue"
/> />
</div> </div>
<div className="col-sm-6 col-lg-3"> <div className="col-sm-6 col-lg-3">
<StatsCard <StatsCard
total={numBlockedFiltering} total={numBlockedFiltering}
lineData={getNormalizedHistory(blockedFiltering, interval, 'blockedFiltering')} lineData={getNormalizedHistory(blockedFiltering, interval, 'blockedFiltering')}
percent={getPercent(numDnsQueries, numBlockedFiltering)} percent={getPercent(numDnsQueries, numBlockedFiltering)}
title={ title={<Trans components={[<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED.QUERY}`} key="0">link</Link>]}>blocked_by</Trans>}
<Trans
components={[
<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED.QUERY}`} key="0">
link
</Link>,
]}>
blocked_by
</Trans>
}
color="red" color="red"
/> />
</div> </div>
<div className="col-sm-6 col-lg-3"> <div className="col-sm-6 col-lg-3">
<StatsCard <StatsCard
total={numReplacedSafebrowsing} total={numReplacedSafebrowsing}
lineData={getNormalizedHistory(replacedSafebrowsing, interval, 'replacedSafebrowsing')} lineData={getNormalizedHistory(
replacedSafebrowsing,
interval,
'replacedSafebrowsing',
)}
percent={getPercent(numDnsQueries, numReplacedSafebrowsing)} percent={getPercent(numDnsQueries, numReplacedSafebrowsing)}
title={ title={<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_THREATS.QUERY}`}><Trans>stats_malware_phishing</Trans></Link>}
<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_THREATS.QUERY}`}>
<Trans>stats_malware_phishing</Trans>
</Link>
}
color="green" color="green"
/> />
</div> </div>
<div className="col-sm-6 col-lg-3"> <div className="col-sm-6 col-lg-3">
<StatsCard <StatsCard
total={numReplacedParental} total={numReplacedParental}
lineData={getNormalizedHistory(replacedParental, interval, 'replacedParental')} lineData={getNormalizedHistory(replacedParental, interval, 'replacedParental')}
percent={getPercent(numDnsQueries, numReplacedParental)} percent={getPercent(numDnsQueries, numReplacedParental)}
title={ title={<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY}`}><Trans>stats_adult</Trans></Link>}
<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY}`}>
<Trans>stats_adult</Trans>
</Link>
}
color="yellow" color="yellow"
/> />
</div> </div>
</div> </div>
); );
Statistics.propTypes = {
interval: PropTypes.number.isRequired,
dnsQueries: PropTypes.array.isRequired,
blockedFiltering: PropTypes.array.isRequired,
replacedSafebrowsing: PropTypes.array.isRequired,
replacedParental: PropTypes.array.isRequired,
numDnsQueries: PropTypes.number.isRequired,
numBlockedFiltering: PropTypes.number.isRequired,
numReplacedSafebrowsing: PropTypes.number.isRequired,
numReplacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
};
export default withTranslation()(Statistics); export default withTranslation()(Statistics);

View File

@@ -1,34 +1,38 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { STATUS_COLORS } from '../../helpers/constants'; import { STATUS_COLORS } from '../../helpers/constants';
import { formatNumber } from '../../helpers/helpers'; import { formatNumber } from '../../helpers/helpers';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Line from '../ui/Line'; import Line from '../ui/Line';
interface StatsCardProps { const StatsCard = ({
total: number; total, lineData, percent, title, color,
lineData: unknown[]; }) => (
title: object;
color: string;
percent?: number;
}
const StatsCard = ({ total, lineData, percent, title, color }: StatsCardProps) => (
<Card type="card--full" bodyType="card-wrap"> <Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats"> <div className="card-body-stats">
<div className={`card-value card-value-stats text-${color}`}>{formatNumber(total)}</div> <div className={`card-value card-value-stats text-${color}`}>
{formatNumber(total)}
</div>
<div className="card-title-stats">{title}</div> <div className="card-title-stats">{title}</div>
</div> </div>
{percent >= 0 && <div className={`card-value card-value-percent text-${color}`}>{percent}</div>} {percent >= 0 && (
<div className={`card-value card-value-percent text-${color}`}>
{percent}
</div>
)}
<div className="card-chart-bg"> <div className="card-chart-bg">
<Line data={lineData} color={STATUS_COLORS[color]} /> <Line data={lineData} color={STATUS_COLORS[color]} />
</div> </div>
</Card> </Card>
); );
StatsCard.propTypes = {
total: PropTypes.number.isRequired,
lineData: PropTypes.array.isRequired,
title: PropTypes.object.isRequired,
color: PropTypes.string.isRequired,
percent: PropTypes.number,
};
export default StatsCard; export default StatsCard;

View File

@@ -1,47 +1,50 @@
import React from 'react'; import React from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import round from 'lodash/round'; import round from 'lodash/round';
import { withTranslation, Trans } from 'react-i18next'; import { withTranslation, Trans } from 'react-i18next';
import { TFunction } from 'i18next';
import Card from '../ui/Card'; import Card from '../ui/Card';
import DomainCell from './DomainCell'; import DomainCell from './DomainCell';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, TABLES_MIN_ROWS } from '../../helpers/constants'; import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, TABLES_MIN_ROWS } from '../../helpers/constants';
interface TimeCellProps { const TimeCell = ({ value }) => {
value?: string | number;
}
const TimeCell = ({ value }: TimeCellProps) => {
if (!value) { if (!value) {
return ''; return '';
} }
const valueInMilliseconds = round(Number(value) * 1000); const valueInMilliseconds = round(value * 1000);
return ( return (
<div className="logs__row o-hidden"> <div className="logs__row o-hidden">
<span className="logs__text logs__text--full" title={valueInMilliseconds.toString()}> <span className="logs__text logs__text--full" title={valueInMilliseconds}>
{valueInMilliseconds}&nbsp;ms {valueInMilliseconds}&nbsp;ms
</span> </span>
</div> </div>
); );
}; };
interface UpstreamAvgTimeProps { TimeCell.propTypes = {
topUpstreamsAvgTime: { name: string; count: number }[]; value: PropTypes.oneOfType([
refreshButton: React.ReactNode; PropTypes.string,
subtitle: string; PropTypes.number,
t: TFunction; ]),
} };
const UpstreamAvgTime = ({ t, refreshButton, topUpstreamsAvgTime, subtitle }: UpstreamAvgTimeProps) => ( const UpstreamAvgTime = ({
<Card title={t('average_upstream_response_time')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}> t,
refreshButton,
topUpstreamsAvgTime,
subtitle,
}) => (
<Card
title={t('average_upstream_response_time')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<ReactTable <ReactTable
data={topUpstreamsAvgTime.map(({ name: domain, count }: { name: string; count: number }) => ({ data={topUpstreamsAvgTime.map(({ name: domain, count }) => ({
domain, domain,
count, count,
}))} }))}
@@ -67,4 +70,11 @@ const UpstreamAvgTime = ({ t, refreshButton, topUpstreamsAvgTime, subtitle }: Up
</Card> </Card>
); );
UpstreamAvgTime.propTypes = {
topUpstreamsAvgTime: PropTypes.array.isRequired,
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(UpstreamAvgTime); export default withTranslation()(UpstreamAvgTime);

View File

@@ -1,47 +1,51 @@
import React from 'react'; import React from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next'; import { withTranslation, Trans } from 'react-i18next';
import { TFunction } from 'i18next';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Cell from '../ui/Cell'; import Cell from '../ui/Cell';
import DomainCell from './DomainCell'; import DomainCell from './DomainCell';
import { getPercent } from '../../helpers/helpers'; import { getPercent } from '../../helpers/helpers';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants'; import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
const CountCell = (totalBlocked: any) => const CountCell = (totalBlocked) => (
function cell(row: any) { function cell(row) {
const { value } = row; const { value } = row;
const percent = getPercent(totalBlocked, value); const percent = getPercent(totalBlocked, value);
return <Cell value={value} percent={percent} color={STATUS_COLORS.green} />; return (
}; <Cell
value={value}
percent={percent}
color={STATUS_COLORS.green}
/>
);
}
);
const getTotalUpstreamRequests = (stats: any) => { const getTotalUpstreamRequests = (stats) => {
let total = 0; let total = 0;
stats.forEach(({ count }: any) => { stats.forEach(({ count }) => { total += count; });
total += count;
});
return total; return total;
}; };
interface UpstreamResponsesProps { const UpstreamResponses = ({
topUpstreamsResponses: { name: string; count: number }[]; t,
refreshButton: React.ReactNode; refreshButton,
subtitle: string; topUpstreamsResponses,
t: TFunction; subtitle,
} }) => (
<Card
const UpstreamResponses = ({ t, refreshButton, topUpstreamsResponses, subtitle }: UpstreamResponsesProps) => ( title={t('top_upstreams')}
<Card title={t('top_upstreams')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}> subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<ReactTable <ReactTable
data={topUpstreamsResponses.map(({ name: domain, count }: { name: string; count: number }) => ({ data={topUpstreamsResponses.map(({ name: domain, count }) => ({
domain, domain,
count, count,
}))} }))}
@@ -67,4 +71,11 @@ const UpstreamResponses = ({ t, refreshButton, topUpstreamsResponses, subtitle }
</Card> </Card>
); );
UpstreamResponses.propTypes = {
topUpstreamsResponses: PropTypes.array.isRequired,
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(UpstreamResponses); export default withTranslation()(UpstreamResponses);

View File

@@ -0,0 +1,276 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { HashLink as Link } from 'react-router-hash-link';
import { Trans, useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Statistics from './Statistics';
import Counters from './Counters';
import Clients from './Clients';
import QueriedDomains from './QueriedDomains';
import BlockedDomains from './BlockedDomains';
import {
DISABLE_PROTECTION_TIMINGS,
ONE_SECOND_IN_MS,
SETTINGS_URLS,
TIME_UNITS,
} from '../../helpers/constants';
import {
msToSeconds,
msToMinutes,
msToHours,
msToDays,
} from '../../helpers/helpers';
import PageTitle from '../ui/PageTitle';
import Loading from '../ui/Loading';
import './Dashboard.css';
import Dropdown from '../ui/Dropdown';
import UpstreamResponses from './UpstreamResponses';
import UpstreamAvgTime from './UpstreamAvgTime';
const Dashboard = ({
getAccessList,
getStats,
getStatsConfig,
dashboard,
dashboard: { protectionEnabled, processingProtection, protectionDisabledDuration },
toggleProtection,
stats,
access,
}) => {
const { t } = useTranslation();
const getAllStats = () => {
getAccessList();
getStats();
getStatsConfig();
};
useEffect(() => {
getAllStats();
}, []);
const getSubtitle = () => {
if (!stats.enabled) {
return t('stats_disabled_short');
}
const msIn7Days = 604800000;
if (stats.timeUnits === TIME_UNITS.HOURS && stats.interval === msIn7Days) {
return t('for_last_days', { count: msToDays(stats.interval) });
}
return stats.timeUnits === TIME_UNITS.HOURS
? t('for_last_hours', { count: msToHours(stats.interval) })
: t('for_last_days', { count: msToDays(stats.interval) });
};
const buttonClass = classNames('btn btn-sm dashboard-protection-button', {
'btn-gray': protectionEnabled,
'btn-success': !protectionEnabled,
});
const refreshButton = <button
type="button"
className="btn btn-icon btn-outline-primary btn-sm"
title={t('refresh_btn')}
onClick={() => getAllStats()}
>
<svg className="icons icon12">
<use xlinkHref="#refresh" />
</svg>
</button>;
const statsProcessing = stats.processingStats
|| stats.processingGetConfig
|| access.processing;
const subtitle = getSubtitle();
const DISABLE_PROTECTION_ITEMS = [
{
text: t('disable_for_seconds', { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HALF_MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) }),
disableTime: DISABLE_PROTECTION_TIMINGS.TEN_MINUTES,
},
{
text: t('disable_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HOUR,
},
{
text: t('disable_until_tomorrow'),
disableTime: DISABLE_PROTECTION_TIMINGS.TOMORROW,
},
];
const getDisableProtectionItems = () => (
Object.values(DISABLE_PROTECTION_ITEMS)
.map((item, index) => (
<div
key={`disable_timings_${index}`}
className="dropdown-item"
onClick={() => {
toggleProtection(protectionEnabled, item.disableTime - ONE_SECOND_IN_MS);
}}
>
{item.text}
</div>
))
);
const getRemaningTimeText = (milliseconds) => {
if (!milliseconds) {
return '';
}
const date = new Date(milliseconds);
const hh = date.getUTCHours();
const mm = `0${date.getUTCMinutes()}`.slice(-2);
const ss = `0${date.getUTCSeconds()}`.slice(-2);
const formattedHH = `0${hh}`.slice(-2);
return hh ? `${formattedHH}:${mm}:${ss}` : `${mm}:${ss}`;
};
const getProtectionBtnText = (status) => (status ? t('disable_protection') : t('enable_protection'));
return <>
<PageTitle title={t('dashboard')} containerClass="page-title--dashboard">
<div className="page-title__protection">
<button
type="button"
className={buttonClass}
onClick={() => {
toggleProtection(protectionEnabled);
}}
disabled={processingProtection}
>
{protectionDisabledDuration
? `${t('enable_protection_timer')} ${getRemaningTimeText(protectionDisabledDuration)}`
: getProtectionBtnText(protectionEnabled)
}
</button>
{protectionEnabled && <Dropdown
label=""
baseClassName="dropdown-protection"
icon="arrow-down"
controlClassName="dropdown-protection__toggle"
menuClassName="dropdown-menu dropdown-menu-arrow dropdown-menu--protection"
>
{getDisableProtectionItems()}
</Dropdown>}
</div>
<button
type="button"
className="btn btn-outline-primary btn-sm"
onClick={getAllStats}
>
<Trans>refresh_statics</Trans>
</button>
</PageTitle>
{statsProcessing && <Loading />}
{!statsProcessing && <div className="row row-cards dashboard">
<div className="col-lg-12">
{stats.interval === 0 && (
<div className="alert alert-warning" role="alert">
<Trans components={[
<Link
to={`${SETTINGS_URLS.settings}#stats-config`}
key="0"
>
link
</Link>,
]}>
stats_disabled
</Trans>
</div>
)}
<Statistics
interval={msToDays(stats.interval)}
dnsQueries={stats.dnsQueries}
blockedFiltering={stats.blockedFiltering}
replacedSafebrowsing={stats.replacedSafebrowsing}
replacedParental={stats.replacedParental}
numDnsQueries={stats.numDnsQueries}
numBlockedFiltering={stats.numBlockedFiltering}
numReplacedSafebrowsing={stats.numReplacedSafebrowsing}
numReplacedParental={stats.numReplacedParental}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<Counters
subtitle={subtitle}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<Clients
subtitle={subtitle}
dnsQueries={stats.numDnsQueries}
topClients={stats.topClients}
clients={dashboard.clients}
autoClients={dashboard.autoClients}
refreshButton={refreshButton}
processingAccessSet={access.processingSet}
disallowedClients={access.disallowed_clients}
/>
</div>
<div className="col-lg-6">
<QueriedDomains
subtitle={subtitle}
dnsQueries={stats.numDnsQueries}
topQueriedDomains={stats.topQueriedDomains}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<BlockedDomains
subtitle={subtitle}
topBlockedDomains={stats.topBlockedDomains}
blockedFiltering={stats.numBlockedFiltering}
replacedSafebrowsing={stats.numReplacedSafebrowsing}
replacedSafesearch={stats.numReplacedSafesearch}
replacedParental={stats.numReplacedParental}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<UpstreamResponses
subtitle={subtitle}
topUpstreamsResponses={stats.topUpstreamsResponses}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<UpstreamAvgTime
subtitle={subtitle}
topUpstreamsAvgTime={stats.topUpstreamsAvgTime}
refreshButton={refreshButton}
/>
</div>
</div>}
</>;
};
Dashboard.propTypes = {
dashboard: PropTypes.object.isRequired,
stats: PropTypes.object.isRequired,
access: PropTypes.object.isRequired,
getStats: PropTypes.func.isRequired,
getStatsConfig: PropTypes.func.isRequired,
toggleProtection: PropTypes.func.isRequired,
getClients: PropTypes.func.isRequired,
getAccessList: PropTypes.func.isRequired,
};
export default Dashboard;

View File

@@ -1,260 +0,0 @@
import React, { useEffect } from 'react';
import { HashLink as Link } from 'react-router-hash-link';
import { Trans, useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Statistics from './Statistics';
import Counters from './Counters';
import Clients from './Clients';
import QueriedDomains from './QueriedDomains';
import BlockedDomains from './BlockedDomains';
import { DISABLE_PROTECTION_TIMINGS, ONE_SECOND_IN_MS, SETTINGS_URLS, TIME_UNITS } from '../../helpers/constants';
import { msToSeconds, msToMinutes, msToHours, msToDays } from '../../helpers/helpers';
import PageTitle from '../ui/PageTitle';
import Loading from '../ui/Loading';
import './Dashboard.css';
import Dropdown from '../ui/Dropdown';
import UpstreamResponses from './UpstreamResponses';
import UpstreamAvgTime from './UpstreamAvgTime';
import { AccessData, DashboardData, StatsData } from '../../initialState';
interface DashboardProps {
dashboard: DashboardData;
stats: StatsData;
access: AccessData;
getStats: (...args: unknown[]) => unknown;
getStatsConfig: (...args: unknown[]) => unknown;
toggleProtection: (...args: unknown[]) => unknown;
getClients: (...args: unknown[]) => unknown;
getAccessList: () => (dispatch: any) => void;
}
const Dashboard = ({
getAccessList,
getStats,
getStatsConfig,
dashboard: { protectionEnabled, processingProtection, protectionDisabledDuration },
toggleProtection,
stats,
access,
}: DashboardProps) => {
const { t } = useTranslation();
const getAllStats = () => {
getAccessList();
getStats();
getStatsConfig();
};
useEffect(() => {
getAllStats();
}, []);
const getSubtitle = () => {
if (!stats.enabled) {
return t('stats_disabled_short');
}
const msIn7Days = 604800000;
if (stats.timeUnits === TIME_UNITS.HOURS && stats.interval === msIn7Days) {
return t('for_last_days', { count: msToDays(stats.interval) });
}
return stats.timeUnits === TIME_UNITS.HOURS
? t('for_last_hours', { count: msToHours(stats.interval) })
: t('for_last_days', { count: msToDays(stats.interval) });
};
const buttonClass = classNames('btn btn-sm dashboard-protection-button', {
'btn-gray': protectionEnabled,
'btn-success': !protectionEnabled,
});
const refreshButton = (
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm"
title={t('refresh_btn')}
onClick={() => getAllStats()}>
<svg className="icons icon12">
<use xlinkHref="#refresh" />
</svg>
</button>
);
const statsProcessing = stats.processingStats || stats.processingGetConfig || access.processing;
const subtitle = getSubtitle();
const DISABLE_PROTECTION_ITEMS = [
{
text: t('disable_for_seconds', { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HALF_MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) }),
disableTime: DISABLE_PROTECTION_TIMINGS.TEN_MINUTES,
},
{
text: t('disable_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HOUR,
},
{
text: t('disable_until_tomorrow'),
disableTime: DISABLE_PROTECTION_TIMINGS.TOMORROW,
},
];
const getDisableProtectionItems = () =>
Object.values(DISABLE_PROTECTION_ITEMS).map((item: any, index: any) => (
<div
key={`disable_timings_${index}`}
className="dropdown-item"
onClick={() => {
toggleProtection(protectionEnabled, item.disableTime - ONE_SECOND_IN_MS);
}}>
{item.text}
</div>
));
const getRemaningTimeText = (milliseconds: any) => {
if (!milliseconds) {
return '';
}
const date = new Date(milliseconds);
const hh = date.getUTCHours();
const mm = `0${date.getUTCMinutes()}`.slice(-2);
const ss = `0${date.getUTCSeconds()}`.slice(-2);
const formattedHH = `0${hh}`.slice(-2);
return hh ? `${formattedHH}:${mm}:${ss}` : `${mm}:${ss}`;
};
const getProtectionBtnText = (status: any) => (status ? t('disable_protection') : t('enable_protection'));
return (
<>
<PageTitle title={t('dashboard')} containerClass="page-title--dashboard">
<div className="page-title__protection">
<button
type="button"
className={buttonClass}
onClick={() => {
toggleProtection(protectionEnabled);
}}
disabled={processingProtection}>
{protectionDisabledDuration
? `${t('enable_protection_timer')} ${getRemaningTimeText(protectionDisabledDuration)}`
: getProtectionBtnText(protectionEnabled)}
</button>
{protectionEnabled && (
<Dropdown
label=""
baseClassName="dropdown-protection"
icon="arrow-down"
controlClassName="dropdown-protection__toggle"
menuClassName="dropdown-menu dropdown-menu-arrow dropdown-menu--protection">
{getDisableProtectionItems()}
</Dropdown>
)}
</div>
<button type="button" className="btn btn-outline-primary btn-sm" onClick={getAllStats}>
<Trans>refresh_statics</Trans>
</button>
</PageTitle>
{statsProcessing && <Loading />}
{!statsProcessing && (
<div className="row row-cards dashboard">
<div className="col-lg-12">
{stats.interval === 0 && (
<div className="alert alert-warning" role="alert">
<Trans
components={[
<Link to={`${SETTINGS_URLS.settings}#stats-config`} key="0">
link
</Link>,
]}>
stats_disabled
</Trans>
</div>
)}
<Statistics
interval={msToDays(stats.interval)}
dnsQueries={stats.dnsQueries}
blockedFiltering={stats.blockedFiltering}
replacedSafebrowsing={stats.replacedSafebrowsing}
replacedParental={stats.replacedParental}
numDnsQueries={stats.numDnsQueries}
numBlockedFiltering={stats.numBlockedFiltering}
numReplacedSafebrowsing={stats.numReplacedSafebrowsing}
numReplacedParental={stats.numReplacedParental}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<Counters subtitle={subtitle} refreshButton={refreshButton} />
</div>
<div className="col-lg-6">
<Clients subtitle={subtitle} refreshButton={refreshButton} />
</div>
<div className="col-lg-6">
<QueriedDomains
subtitle={subtitle}
dnsQueries={stats.numDnsQueries}
topQueriedDomains={stats.topQueriedDomains}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<BlockedDomains
subtitle={subtitle}
topBlockedDomains={stats.topBlockedDomains}
blockedFiltering={stats.numBlockedFiltering}
replacedSafebrowsing={stats.numReplacedSafebrowsing}
replacedSafesearch={stats.numReplacedSafesearch}
replacedParental={stats.numReplacedParental}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<UpstreamResponses
subtitle={subtitle}
topUpstreamsResponses={stats.topUpstreamsResponses}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<UpstreamAvgTime
subtitle={subtitle}
topUpstreamsAvgTime={stats.topUpstreamsAvgTime}
refreshButton={refreshButton}
/>
</div>
</div>
)}
</>
);
};
export default Dashboard;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next';
const Actions = ({
handleAdd, handleRefresh, processingRefreshFilters, whitelist,
}) => <div className="card-actions">
<button
className="btn btn-success btn-standard mr-2 btn-large mb-2"
type="submit"
onClick={handleAdd}
>
{whitelist ? <Trans>add_allowlist</Trans> : <Trans>add_blocklist</Trans>}
</button>
<button
className="btn btn-primary btn-standard mb-2"
type="submit"
onClick={handleRefresh}
disabled={processingRefreshFilters}
>
<Trans>check_updates_btn</Trans>
</button>
</div>;
Actions.propTypes = {
handleAdd: PropTypes.func.isRequired,
handleRefresh: PropTypes.func.isRequired,
processingRefreshFilters: PropTypes.bool.isRequired,
whitelist: PropTypes.bool,
};
export default withTranslation()(Actions);

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { withTranslation, Trans } from 'react-i18next';
interface ActionsProps {
handleAdd: (...args: unknown[]) => unknown;
handleRefresh: (...args: unknown[]) => unknown;
processingRefreshFilters: boolean;
whitelist?: boolean;
}
const Actions = ({ handleAdd, handleRefresh, processingRefreshFilters, whitelist }: ActionsProps) => (
<div className="card-actions">
<button className="btn btn-success btn-standard mr-2 btn-large mb-2" type="submit" onClick={handleAdd}>
{whitelist ? <Trans>add_allowlist</Trans> : <Trans>add_blocklist</Trans>}
</button>
<button
className="btn btn-primary btn-standard mb-2"
type="submit"
onClick={handleRefresh}
disabled={processingRefreshFilters}>
<Trans>check_updates_btn</Trans>
</button>
</div>
);
export default withTranslation()(Actions);

View File

@@ -15,12 +15,10 @@ import {
getRulesToFilterList, getRulesToFilterList,
} from '../../../helpers/helpers'; } from '../../../helpers/helpers';
import { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants'; import { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants';
import { toggleBlocking } from '../../../actions'; import { toggleBlocking } from '../../../actions';
import { RootState } from '../../../initialState';
const renderBlockingButton = (isFiltered: any, domain: any) => { const renderBlockingButton = (isFiltered, domain) => {
const processingRules = useSelector((state: RootState) => state.filtering.processingRules); const processingRules = useSelector((state) => state.filtering.processingRules);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -30,32 +28,28 @@ const renderBlockingButton = (isFiltered: any, domain: any) => {
await dispatch(toggleBlocking(buttonType, domain)); await dispatch(toggleBlocking(buttonType, domain));
}; };
const buttonClass = classNames( const buttonClass = classNames('mt-3 button-action button-action--main button-action--active button-action--small', {
'mt-3 button-action button-action--main button-action--active button-action--small', 'button-action--unblock': isFiltered,
{ });
'button-action--unblock': isFiltered,
},
);
return ( return <button type="button"
<button type="button" className={buttonClass} onClick={onClick} disabled={processingRules}> className={buttonClass}
onClick={onClick}
disabled={processingRules}
>
{t(buttonType)} {t(buttonType)}
</button> </button>;
);
}; };
const getTitle = () => { const getTitle = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const filters = useSelector((state: RootState) => state.filtering.filters, shallowEqual); const filters = useSelector((state) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
const rules = useSelector((state) => state.filtering.check.rules, shallowEqual);
const reason = useSelector((state) => state.filtering.check.reason);
const whitelistFilters = useSelector((state: RootState) => state.filtering.whitelistFilters, shallowEqual); const getReasonFiltered = (reason) => {
const rules = useSelector((state: RootState) => state.filtering.check.rules, shallowEqual);
const reason = useSelector((state: RootState) => state.filtering.check.reason);
const getReasonFiltered = (reason: any) => {
const filterKey = reason.replace(FILTERED, ''); const filterKey = reason.replace(FILTERED, '');
return i18next.t('query_log_filtered', { filter: filterKey }); return i18next.t('query_log_filtered', { filter: filterKey });
}; };
@@ -77,23 +71,24 @@ const getTitle = () => {
return REASON_TO_TITLE_MAP[reason]; return REASON_TO_TITLE_MAP[reason];
} }
return ( return <>
<> <div>{t('check_reason', { reason })}</div>
<div>{t('check_reason', { reason })}</div> <div>
{t('rule_label')}:
<div> &nbsp;
{t('rule_label')}: &nbsp; {ruleAndFilterNames}
{ruleAndFilterNames} </div>
</div> </>;
</>
);
}; };
const Info = () => { const Info = () => {
const { hostname, reason, service_name, cname, ip_addrs } = useSelector( const {
(state: RootState) => state.filtering.check, hostname,
shallowEqual, reason,
); service_name,
cname,
ip_addrs,
} = useSelector((state) => state.filtering.check, shallowEqual);
const { t } = useTranslation(); const { t } = useTranslation();
const title = getTitle(); const title = getTitle();
@@ -104,29 +99,23 @@ const Info = () => {
'logs__row--green': checkWhiteList(reason), 'logs__row--green': checkWhiteList(reason),
}); });
const onlyFiltered = checkSafeSearch(reason) || checkSafeBrowsing(reason) || checkParental(reason); const onlyFiltered = checkSafeSearch(reason)
|| checkSafeBrowsing(reason)
|| checkParental(reason);
const isFiltered = checkFiltered(reason); const isFiltered = checkFiltered(reason);
return ( return <div className={className}>
<div className={className}> <div><strong>{hostname}</strong></div>
<div> <div>{title}</div>
<strong>{hostname}</strong> {!onlyFiltered
</div> && <>
{service_name && <div>{t('check_service', { service: service_name })}</div>}
<div>{title}</div> {cname && <div>{t('check_cname', { cname })}</div>}
{!onlyFiltered && ( {ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>}
<> {renderBlockingButton(isFiltered, hostname)}
{service_name && <div>{t('check_service', { service: service_name })}</div>} </>}
</div>;
{cname && <div>{t('check_cname', { cname })}</div>}
{ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>}
{renderBlockingButton(isFiltered, hostname)}
</>
)}
</div>
);
}; };
export default Info; export default Info;

View File

@@ -0,0 +1,66 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Field, reduxForm } from 'redux-form';
import { useSelector } from 'react-redux';
import Card from '../../ui/Card';
import { renderInputField } from '../../../helpers/form';
import Info from './Info';
import { FORM_NAME } from '../../../helpers/constants';
const Check = (props) => {
const {
pristine,
invalid,
handleSubmit,
} = props;
const { t } = useTranslation();
const processingCheck = useSelector((state) => state.filtering.processingCheck);
const hostname = useSelector((state) => state.filtering.check.hostname);
return <Card
title={t('check_title')}
subtitle={t('check_desc')}
>
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-md-6">
<div className="input-group">
<Field
id="name"
name="name"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('form_enter_host')}
/>
<span className="input-group-append">
<button
className="btn btn-success btn-standard btn-large"
type="submit"
onClick={handleSubmit}
disabled={pristine || invalid || processingCheck}
>
{t('check')}
</button>
</span>
</div>
{hostname && <>
<hr />
<Info />
</>}
</div>
</div>
</form>
</Card>;
};
Check.propTypes = {
handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
};
export default reduxForm({ form: FORM_NAME.DOMAIN_CHECK })(Check);

View File

@@ -1,70 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Field, reduxForm } from 'redux-form';
import { useSelector } from 'react-redux';
import Card from '../../ui/Card';
import { renderInputField } from '../../../helpers/form';
import Info from './Info';
import { FORM_NAME } from '../../../helpers/constants';
import { RootState } from '../../../initialState';
interface CheckProps {
handleSubmit: (...args: unknown[]) => string;
pristine: boolean;
invalid: boolean;
}
const Check = (props: CheckProps) => {
const { pristine, invalid, handleSubmit } = props;
const { t } = useTranslation();
const processingCheck = useSelector((state: RootState) => state.filtering.processingCheck);
const hostname = useSelector((state: RootState) => state.filtering.check.hostname);
return (
<Card title={t('check_title')} subtitle={t('check_desc')}>
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-md-6">
<div className="input-group">
<Field
id="name"
name="name"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('form_enter_host')}
/>
<span className="input-group-append">
<button
className="btn btn-success btn-standard btn-large"
type="submit"
onClick={handleSubmit}
disabled={pristine || invalid || processingCheck}>
{t('check')}
</button>
</span>
</div>
{hostname && (
<>
<hr />
<Info />
</>
)}
</div>
</div>
</form>
</Card>
);
};
export default reduxForm({ form: FORM_NAME.DOMAIN_CHECK })(Check);

View File

@@ -1,46 +1,32 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import Card from '../ui/Card'; import Card from '../ui/Card';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
import Examples from './Examples'; import Examples from './Examples';
import Check from './Check'; import Check from './Check';
import { getTextareaCommentsHighlight, syncScroll } from '../../helpers/highlightTextareaComments'; import { getTextareaCommentsHighlight, syncScroll } from '../../helpers/highlightTextareaComments';
import { COMMENT_LINE_DEFAULT_TOKEN } from '../../helpers/constants'; import { COMMENT_LINE_DEFAULT_TOKEN } from '../../helpers/constants';
import '../ui/texareaCommentsHighlight.css'; import '../ui/texareaCommentsHighlight.css';
import { FilteringData } from '../../initialState';
interface CustomRulesProps { class CustomRules extends Component {
filtering: FilteringData;
setRules: (...args: unknown[]) => unknown;
checkHost: (...args: unknown[]) => string;
getFilteringStatus: (...args: unknown[]) => unknown;
handleRulesChange: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
}
class CustomRules extends Component<CustomRulesProps> {
ref = React.createRef(); ref = React.createRef();
componentDidMount() { componentDidMount() {
this.props.getFilteringStatus(); this.props.getFilteringStatus();
} }
handleChange = (e: any) => { handleChange = (e) => {
const { value } = e.currentTarget; const { value } = e.currentTarget;
this.handleRulesChange(value); this.handleRulesChange(value);
}; };
handleSubmit = (e: any) => { handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
this.handleRulesSubmit(); this.handleRulesSubmit();
}; };
handleRulesChange = (value: any) => { handleRulesChange = (value) => {
this.props.handleRulesChange({ userRules: value }); this.props.handleRulesChange({ userRules: value });
}; };
@@ -48,22 +34,23 @@ class CustomRules extends Component<CustomRulesProps> {
this.props.setRules(this.props.filtering.userRules); this.props.setRules(this.props.filtering.userRules);
}; };
handleCheck = (values: any) => { handleCheck = (values) => {
this.props.checkHost(values); this.props.checkHost(values);
}; };
onScroll = (e: any) => syncScroll(e, this.ref); onScroll = (e) => syncScroll(e, this.ref)
render() { render() {
const { const {
t, t,
filtering: { userRules }, filtering: {
userRules,
},
} = this.props; } = this.props;
return ( return (
<> <>
<PageTitle title={t('custom_filtering_rules')} /> <PageTitle title={t('custom_filtering_rules')} />
<Card subtitle={t('custom_filter_rules_hint')}> <Card subtitle={t('custom_filter_rules_hint')}>
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<div className="text-edit-container mb-4"> <div className="text-edit-container mb-4">
@@ -73,31 +60,39 @@ class CustomRules extends Component<CustomRulesProps> {
onChange={this.handleChange} onChange={this.handleChange}
onScroll={this.onScroll} onScroll={this.onScroll}
/> />
{getTextareaCommentsHighlight(this.ref, userRules, [ {getTextareaCommentsHighlight(
COMMENT_LINE_DEFAULT_TOKEN, this.ref,
'!', userRules,
])} undefined,
[COMMENT_LINE_DEFAULT_TOKEN, '!'],
)}
</div> </div>
<div className="card-actions"> <div className="card-actions">
<button <button
className="btn btn-success btn-standard btn-large" className="btn btn-success btn-standard btn-large"
type="submit" type="submit"
onClick={this.handleSubmit}> onClick={this.handleSubmit}
>
<Trans>apply_btn</Trans> <Trans>apply_btn</Trans>
</button> </button>
</div> </div>
</form> </form>
<hr /> <hr />
<Examples /> <Examples />
</Card> </Card>
<Check onSubmit={this.handleCheck} /> <Check onSubmit={this.handleCheck} />
</> </>
); );
} }
} }
CustomRules.propTypes = {
filtering: PropTypes.object.isRequired,
setRules: PropTypes.func.isRequired,
checkHost: PropTypes.func.isRequired,
getFilteringStatus: PropTypes.func.isRequired,
handleRulesChange: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(CustomRules); export default withTranslation()(CustomRules);

View File

@@ -1,4 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
@@ -8,41 +9,15 @@ import Actions from './Actions';
import Table from './Table'; import Table from './Table';
import { MODAL_TYPE } from '../../helpers/constants'; import { MODAL_TYPE } from '../../helpers/constants';
import { getCurrentFilter } from '../../helpers/helpers'; import { getCurrentFilter } from '../../helpers/helpers';
interface DnsAllowlistProps { class DnsAllowlist extends Component {
getFilteringStatus: (...args: unknown[]) => unknown;
filtering: {
modalType: string;
modalFilterUrl: string;
isModalOpen: boolean;
isFilterAdded: boolean;
processingRefreshFilters: boolean;
processingRemoveFilter: boolean;
processingAddFilter: boolean;
processingConfigFilter: boolean;
processingFilters: boolean;
whitelistFilters: any[];
};
removeFilter: (...args: unknown[]) => unknown;
toggleFilterStatus: (...args: unknown[]) => unknown;
addFilter: (...args: unknown[]) => unknown;
toggleFilteringModal: (...args: unknown[]) => unknown;
handleRulesChange: (...args: unknown[]) => unknown;
refreshFilters: (...args: unknown[]) => unknown;
editFilter: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
}
class DnsAllowlist extends Component<DnsAllowlistProps> {
componentDidMount() { componentDidMount() {
this.props.getFilteringStatus(); this.props.getFilteringStatus();
} }
handleSubmit = (values: any) => { handleSubmit = (values) => {
const { name, url } = values; const { name, url } = values;
const { filtering } = this.props; const { filtering } = this.props;
const whitelist = true; const whitelist = true;
@@ -53,17 +28,15 @@ class DnsAllowlist extends Component<DnsAllowlistProps> {
} }
}; };
handleDelete = (url: any) => { handleDelete = (url) => {
if (window.confirm(this.props.t('list_confirm_delete'))) { if (window.confirm(this.props.t('list_confirm_delete'))) {
const whitelist = true; const whitelist = true;
this.props.removeFilter(url, whitelist); this.props.removeFilter(url, whitelist);
} }
}; };
toggleFilter = (url: any, data: any) => { toggleFilter = (url, data) => {
const whitelist = true; const whitelist = true;
this.props.toggleFilterStatus(url, data, whitelist); this.props.toggleFilterStatus(url, data, whitelist);
}; };
@@ -80,6 +53,7 @@ class DnsAllowlist extends Component<DnsAllowlistProps> {
t, t,
toggleFilteringModal, toggleFilteringModal,
addFilter, addFilter,
toggleFilterStatus,
filtering: { filtering: {
whitelistFilters, whitelistFilters,
isModalOpen, isModalOpen,
@@ -94,18 +68,19 @@ class DnsAllowlist extends Component<DnsAllowlistProps> {
}, },
} = this.props; } = this.props;
const currentFilterData = getCurrentFilter(modalFilterUrl, whitelistFilters); const currentFilterData = getCurrentFilter(modalFilterUrl, whitelistFilters);
const loading = const loading = processingConfigFilter
processingConfigFilter || || processingFilters
processingFilters || || processingAddFilter
processingAddFilter || || processingRemoveFilter
processingRemoveFilter || || processingRefreshFilters;
processingRefreshFilters;
const whitelist = true; const whitelist = true;
return ( return (
<> <>
<PageTitle title={t('dns_allowlists')} subtitle={t('dns_allowlists_desc')} /> <PageTitle
title={t('dns_allowlists')}
subtitle={t('dns_allowlists_desc')}
/>
<div className="content"> <div className="content">
<div className="row"> <div className="row">
<div className="col-md-12"> <div className="col-md-12">
@@ -115,11 +90,11 @@ class DnsAllowlist extends Component<DnsAllowlistProps> {
loading={loading} loading={loading}
processingConfigFilter={processingConfigFilter} processingConfigFilter={processingConfigFilter}
toggleFilteringModal={toggleFilteringModal} toggleFilteringModal={toggleFilteringModal}
toggleFilterStatus={toggleFilterStatus}
handleDelete={this.handleDelete} handleDelete={this.handleDelete}
toggleFilter={this.toggleFilter} toggleFilter={this.toggleFilter}
whitelist={whitelist} whitelist={whitelist}
/> />
<Actions <Actions
handleAdd={this.openAddFiltersModal} handleAdd={this.openAddFiltersModal}
handleRefresh={this.handleRefresh} handleRefresh={this.handleRefresh}
@@ -130,7 +105,6 @@ class DnsAllowlist extends Component<DnsAllowlistProps> {
</div> </div>
</div> </div>
</div> </div>
<Modal <Modal
filters={whitelistFilters} filters={whitelistFilters}
isOpen={isModalOpen} isOpen={isModalOpen}
@@ -149,4 +123,17 @@ class DnsAllowlist extends Component<DnsAllowlistProps> {
} }
} }
DnsAllowlist.propTypes = {
getFilteringStatus: PropTypes.func.isRequired,
filtering: PropTypes.object.isRequired,
removeFilter: PropTypes.func.isRequired,
toggleFilterStatus: PropTypes.func.isRequired,
addFilter: PropTypes.func.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
handleRulesChange: PropTypes.func.isRequired,
refreshFilters: PropTypes.func.isRequired,
editFilter: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(DnsAllowlist); export default withTranslation()(DnsAllowlist);

View File

@@ -1,38 +1,27 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Modal from './Modal'; import Modal from './Modal';
import Actions from './Actions'; import Actions from './Actions';
import Table from './Table'; import Table from './Table';
import { MODAL_TYPE } from '../../helpers/constants'; import { MODAL_TYPE } from '../../helpers/constants';
import { getCurrentFilter } from '../../helpers/helpers'; import {
getCurrentFilter,
} from '../../helpers/helpers';
import filtersCatalog from '../../helpers/filters/filters'; import filtersCatalog from '../../helpers/filters/filters';
import { FilteringData } from '../../initialState';
interface DnsBlocklistProps { class DnsBlocklist extends Component {
getFilteringStatus: (...args: unknown[]) => unknown;
filtering: FilteringData;
removeFilter: (...args: unknown[]) => unknown;
toggleFilterStatus: (...args: unknown[]) => unknown;
addFilter: (...args: unknown[]) => unknown;
toggleFilteringModal: (...args: unknown[]) => unknown;
handleRulesChange: (...args: unknown[]) => unknown;
refreshFilters: (...args: unknown[]) => unknown;
editFilter: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
}
class DnsBlocklist extends Component<DnsBlocklistProps> {
componentDidMount() { componentDidMount() {
this.props.getFilteringStatus(); this.props.getFilteringStatus();
} }
handleSubmit = (values: any) => { handleSubmit = (values) => {
const { modalFilterUrl, modalType } = this.props.filtering; const { modalFilterUrl, modalType } = this.props.filtering;
switch (modalType) { switch (modalType) {
@@ -41,25 +30,23 @@ class DnsBlocklist extends Component<DnsBlocklistProps> {
break; break;
case MODAL_TYPE.ADD_FILTERS: { case MODAL_TYPE.ADD_FILTERS: {
const { name, url } = values; const { name, url } = values;
this.props.addFilter(url, name); this.props.addFilter(url, name);
break; break;
} }
case MODAL_TYPE.CHOOSE_FILTERING_LIST: { case MODAL_TYPE.CHOOSE_FILTERING_LIST: {
const changedValues = Object.entries(values)?.reduce((acc: any, [key, value]) => { const changedValues = Object.entries(values)?.reduce((acc, [key, value]) => {
if (value && key in filtersCatalog.filters) { if (value && key in filtersCatalog.filters) {
acc[key] = value; acc[key] = value;
} }
return acc; return acc;
}, {}); }, {});
Object.keys(changedValues).forEach((fieldName) => { Object.keys(changedValues)
// filterId is actually in the field name .forEach((fieldName) => {
// filterId is actually in the field name
const { source, name } = filtersCatalog.filters[fieldName]; const { source, name } = filtersCatalog.filters[fieldName];
this.props.addFilter(source, name);
this.props.addFilter(source, name); });
});
break; break;
} }
default: default:
@@ -67,13 +54,13 @@ class DnsBlocklist extends Component<DnsBlocklistProps> {
} }
}; };
handleDelete = (url: any) => { handleDelete = (url) => {
if (window.confirm(this.props.t('list_confirm_delete'))) { if (window.confirm(this.props.t('list_confirm_delete'))) {
this.props.removeFilter(url); this.props.removeFilter(url);
} }
}; };
toggleFilter = (url: any, data: any) => { toggleFilter = (url, data) => {
this.props.toggleFilterStatus(url, data); this.props.toggleFilterStatus(url, data);
}; };
@@ -88,11 +75,8 @@ class DnsBlocklist extends Component<DnsBlocklistProps> {
render() { render() {
const { const {
t, t,
toggleFilteringModal, toggleFilteringModal,
addFilter, addFilter,
filtering: { filtering: {
filters, filters,
isModalOpen, isModalOpen,
@@ -107,17 +91,18 @@ class DnsBlocklist extends Component<DnsBlocklistProps> {
}, },
} = this.props; } = this.props;
const currentFilterData = getCurrentFilter(modalFilterUrl, filters); const currentFilterData = getCurrentFilter(modalFilterUrl, filters);
const loading = const loading = processingConfigFilter
processingConfigFilter || || processingFilters
processingFilters || || processingAddFilter
processingAddFilter || || processingRemoveFilter
processingRemoveFilter || || processingRefreshFilters;
processingRefreshFilters;
return ( return (
<> <>
<PageTitle title={t('dns_blocklists')} subtitle={t('dns_blocklists_desc')} /> <PageTitle
title={t('dns_blocklists')}
subtitle={t('dns_blocklists_desc')}
/>
<div className="content"> <div className="content">
<div className="row"> <div className="row">
<div className="col-md-12"> <div className="col-md-12">
@@ -130,7 +115,6 @@ class DnsBlocklist extends Component<DnsBlocklistProps> {
handleDelete={this.handleDelete} handleDelete={this.handleDelete}
toggleFilter={this.toggleFilter} toggleFilter={this.toggleFilter}
/> />
<Actions <Actions
handleAdd={this.openSelectTypeModal} handleAdd={this.openSelectTypeModal}
handleRefresh={this.handleRefresh} handleRefresh={this.handleRefresh}
@@ -140,7 +124,6 @@ class DnsBlocklist extends Component<DnsBlocklistProps> {
</div> </div>
</div> </div>
</div> </div>
<Modal <Modal
filtersCatalog={filtersCatalog} filtersCatalog={filtersCatalog}
filters={filters} filters={filters}
@@ -159,4 +142,17 @@ class DnsBlocklist extends Component<DnsBlocklistProps> {
} }
} }
DnsBlocklist.propTypes = {
getFilteringStatus: PropTypes.func.isRequired,
filtering: PropTypes.object.isRequired,
removeFilter: PropTypes.func.isRequired,
toggleFilterStatus: PropTypes.func.isRequired,
addFilter: PropTypes.func.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
handleRulesChange: PropTypes.func.isRequired,
refreshFilters: PropTypes.func.isRequired,
editFilter: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(DnsBlocklist); export default withTranslation()(DnsBlocklist);

View File

@@ -7,37 +7,31 @@ const Examples = () => (
<Trans>examples_title</Trans>: <Trans>examples_title</Trans>:
<ol className="leading-loose"> <ol className="leading-loose">
<li> <li>
<code>||example.org^</code>:<Trans>example_meaning_filter_block</Trans> <code>||example.org^</code>:
<Trans>example_meaning_filter_block</Trans>
</li> </li>
<li> <li>
<code> @@||example.org^</code>:<Trans>example_meaning_filter_whitelist</Trans> <code> @@||example.org^</code>:
<Trans>example_meaning_filter_whitelist</Trans>
</li> </li>
<li> <li>
<code>127.0.0.1 example.org</code>:<Trans>example_meaning_host_block</Trans> <code>127.0.0.1 example.org</code>:
<Trans>example_meaning_host_block</Trans>
</li> </li>
<li> <li>
<code> <code><Trans>example_comment</Trans></code>:
<Trans>example_comment</Trans> <Trans>example_comment_meaning</Trans>
</code>
:<Trans>example_comment_meaning</Trans>
</li> </li>
<li> <li>
<code> <code><Trans>example_comment_hash</Trans></code>:
<Trans>example_comment_hash</Trans> <Trans>example_comment_meaning</Trans>
</code>
:<Trans>example_comment_meaning</Trans>
</li> </li>
<li> <li>
<code>/REGEX/</code>:<Trans>example_regex_meaning</Trans> <code>/REGEX/</code>:
<Trans>example_regex_meaning</Trans>
</li> </li>
</ol> </ol>
</div> </div>
<p className="mt-1"> <p className="mt-1">
<Trans <Trans
components={[ components={[
@@ -45,10 +39,12 @@ const Examples = () => (
href="https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax&from=ui&app=home" href="https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax&from=ui&app=home"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
key="0"> key="0"
>
link link
</a>, </a>,
]}> ]}
>
filtering_rules_learn_more filtering_rules_learn_more
</Trans> </Trans>
</p> </p>

View File

@@ -0,0 +1,191 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import classNames from 'classnames';
import { validatePath, validateRequiredValue } from '../../helpers/validators';
import { CheckboxField, renderInputField } from '../../helpers/form';
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE, FORM_NAME } from '../../helpers/constants';
import filtersCatalog from '../../helpers/filters/filters';
const getIconsData = (homepage, source) => ([
{
iconName: 'dashboard',
href: homepage,
className: 'ml-1',
},
{
iconName: 'info',
href: source,
},
]);
const renderIcons = (iconsData) => iconsData.map(({
iconName,
href,
className = '',
}) => <a key={iconName} href={href} target="_blank" rel="noopener noreferrer"
className={classNames('d-flex align-items-center', className)}
>
<svg className="icon icon--15 mr-1 icon--gray">
<use xlinkHref={`#${iconName}`} />
</svg>
</a>);
const renderCheckboxField = (
props,
) => <CheckboxField
{...props}
input={{
...props.input,
checked: props.disabled || props.input.checked,
}}
/>;
renderCheckboxField.propTypes = {
// https://redux-form.com/8.3.0/docs/api/field.md/#props
input: PropTypes.object.isRequired,
disabled: PropTypes.bool.isRequired,
};
const renderFilters = ({ categories, filters }, selectedSources, t) => Object.keys(categories)
.map((categoryId) => {
const category = categories[categoryId];
const categoryFilters = [];
Object.keys(filters)
.sort()
.forEach((key) => {
const filter = filters[key];
filter.id = key;
if (filter.categoryId === categoryId) {
categoryFilters.push(filter);
}
});
return <div key={category.name} className="modal-body__item">
<h6 className="font-weight-bold mb-1">{t(category.name)}</h6>
<p className="mb-3">{t(category.description)}</p>
{categoryFilters.map((filter) => {
const { homepage, source, name } = filter;
const isSelected = Object.prototype.hasOwnProperty.call(selectedSources, source);
const iconsData = getIconsData(homepage, source);
return <div key={name} className="d-flex align-items-center pb-1">
<Field
name={filter.id}
type="checkbox"
component={renderCheckboxField}
placeholder={t(name)}
disabled={isSelected}
/>
{renderIcons(iconsData)}
</div>;
})}
</div>;
});
const Form = (props) => {
const {
t,
closeModal,
handleSubmit,
processingAddFilter,
processingConfigFilter,
whitelist,
modalType,
toggleFilteringModal,
selectedSources,
} = props;
const openModal = (modalType, timeout = MODAL_OPEN_TIMEOUT) => {
toggleFilteringModal();
setTimeout(() => toggleFilteringModal({ type: modalType }), timeout);
};
const openFilteringListModal = () => openModal(MODAL_TYPE.CHOOSE_FILTERING_LIST);
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
return <form onSubmit={handleSubmit}>
<div className="modal-body modal-body--filters">
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE
&& <div className="d-flex justify-content-around">
<button onClick={openFilteringListModal}
className="btn btn-success btn-standard mr-2 btn-large">
{t('choose_from_list')}
</button>
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
{t('add_custom_list')}
</button>
</div>}
{modalType === MODAL_TYPE.CHOOSE_FILTERING_LIST
&& renderFilters(filtersCatalog, selectedSources, t)}
{modalType !== MODAL_TYPE.CHOOSE_FILTERING_LIST
&& modalType !== MODAL_TYPE.SELECT_MODAL_TYPE
&& <>
<div className="form__group">
<Field
id="name"
name="name"
type="text"
component={renderInputField}
className="form-control"
placeholder={t('enter_name_hint')}
normalizeOnBlur={(data) => data.trim()}
/>
</div>
<div className="form__group">
<Field
id="url"
name="url"
type="text"
component={renderInputField}
className="form-control"
placeholder={t('enter_url_or_path_hint')}
validate={[validateRequiredValue, validatePath]}
normalizeOnBlur={(data) => data.trim()}
/>
</div>
<div className="form__description">
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
</div>
</>}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={closeModal}
>
{t('cancel_btn')}
</button>
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && <button
type="submit"
className="btn btn-success"
disabled={processingAddFilter || processingConfigFilter}
>
{t('save_btn')}
</button>}
</div>
</form>;
};
Form.propTypes = {
t: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
processingAddFilter: PropTypes.bool.isRequired,
processingConfigFilter: PropTypes.bool.isRequired,
whitelist: PropTypes.bool,
modalType: PropTypes.string.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
selectedSources: PropTypes.object,
};
export default flow([
withTranslation(),
reduxForm({ form: FORM_NAME.FILTER }),
])(Form);

View File

@@ -1,208 +0,0 @@
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import classNames from 'classnames';
import { validatePath, validateRequiredValue } from '../../helpers/validators';
import { CheckboxField, renderInputField } from '../../helpers/form';
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE, FORM_NAME } from '../../helpers/constants';
import filtersCatalog from '../../helpers/filters/filters';
const getIconsData = (homepage: any, source: any) => [
{
iconName: 'dashboard',
href: homepage,
className: 'ml-1',
},
{
iconName: 'info',
href: source,
},
];
const renderIcons = (iconsData: any) =>
iconsData.map(({ iconName, href, className = '' }: any) => (
<a
key={iconName}
href={href}
target="_blank"
rel="noopener noreferrer"
className={classNames('d-flex align-items-center', className)}>
<svg className="icon icon--15 mr-1 icon--gray">
<use xlinkHref={`#${iconName}`} />
</svg>
</a>
));
interface renderCheckboxFieldProps {
// https://redux-form.com/8.3.0/docs/api/field.md/#props
input: {
name: string;
value: string;
checked: boolean;
onChange: (...args: unknown[]) => unknown;
};
disabled: boolean;
}
const renderCheckboxField = (props: renderCheckboxFieldProps) => (
<CheckboxField
{...props}
meta={{ touched: false, error: null }}
input={{
...props.input,
checked: props.disabled || props.input.checked,
}}
/>
);
const renderFilters = ({ categories, filters }: any, selectedSources: any, t: any) =>
Object.keys(categories).map((categoryId) => {
const category = categories[categoryId];
const categoryFilters: any = [];
Object.keys(filters)
.sort()
.forEach((key) => {
const filter = filters[key];
filter.id = key;
if (filter.categoryId === categoryId) {
categoryFilters.push(filter);
}
});
return (
<div key={category.name} className="modal-body__item">
<h6 className="font-weight-bold mb-1">{t(category.name)}</h6>
<p className="mb-3">{t(category.description)}</p>
{categoryFilters.map((filter) => {
const { homepage, source, name } = filter;
const isSelected = Object.prototype.hasOwnProperty.call(selectedSources, source);
const iconsData = getIconsData(homepage, source);
return (
<div key={name} className="d-flex align-items-center pb-1">
<Field
name={filter.id}
type="checkbox"
component={renderCheckboxField}
placeholder={t(name)}
disabled={isSelected}
/>
{renderIcons(iconsData)}
</div>
);
})}
</div>
);
});
interface FormProps {
t: (...args: unknown[]) => string;
closeModal: (...args: unknown[]) => unknown;
handleSubmit: (...args: unknown[]) => string;
processingAddFilter: boolean;
processingConfigFilter: boolean;
whitelist?: boolean;
modalType: string;
toggleFilteringModal: (...args: unknown[]) => unknown;
selectedSources?: object;
}
const Form = (props: FormProps) => {
const {
t,
closeModal,
handleSubmit,
processingAddFilter,
processingConfigFilter,
whitelist,
modalType,
toggleFilteringModal,
selectedSources,
} = props;
const openModal = (modalType: any, timeout = MODAL_OPEN_TIMEOUT) => {
toggleFilteringModal();
setTimeout(() => toggleFilteringModal({ type: modalType }), timeout);
};
const openFilteringListModal = () => openModal(MODAL_TYPE.CHOOSE_FILTERING_LIST);
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
return (
<form onSubmit={handleSubmit}>
<div className="modal-body modal-body--filters">
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (
<div className="d-flex justify-content-around">
<button
onClick={openFilteringListModal}
className="btn btn-success btn-standard mr-2 btn-large">
{t('choose_from_list')}
</button>
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
{t('add_custom_list')}
</button>
</div>
)}
{modalType === MODAL_TYPE.CHOOSE_FILTERING_LIST && renderFilters(filtersCatalog, selectedSources, t)}
{modalType !== MODAL_TYPE.CHOOSE_FILTERING_LIST && modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
<>
<div className="form__group">
<Field
id="name"
name="name"
type="text"
component={renderInputField}
className="form-control"
placeholder={t('enter_name_hint')}
normalizeOnBlur={(data: any) => data.trim()}
/>
</div>
<div className="form__group">
<Field
id="url"
name="url"
type="text"
component={renderInputField}
className="form-control"
placeholder={t('enter_url_or_path_hint')}
validate={[validateRequiredValue, validatePath]}
normalizeOnBlur={(data: any) => data.trim()}
/>
</div>
<div className="form__description">
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
</div>
</>
)}
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={closeModal}>
{t('cancel_btn')}
</button>
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
<button
type="submit"
className="btn btn-success"
disabled={processingAddFilter || processingConfigFilter}>
{t('save_btn')}
</button>
)}
</div>
</form>
);
};
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.FILTER })])(Form);

View File

@@ -1,13 +1,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { MODAL_TYPE } from '../../helpers/constants'; import { MODAL_TYPE } from '../../helpers/constants';
import Form from './Form'; import Form from './Form';
import '../ui/Modal.css'; import '../ui/Modal.css';
import { getMap } from '../../helpers/helpers'; import { getMap } from '../../helpers/helpers';
ReactModal.setAppElement('#root'); ReactModal.setAppElement('#root');
@@ -27,7 +25,7 @@ const MODAL_TYPE_TO_TITLE_TYPE_MAP = {
* @returns {'new_allowlist' | 'edit_allowlist' | 'choose_allowlist' | * @returns {'new_allowlist' | 'edit_allowlist' | 'choose_allowlist' |
* 'new_blocklist' | 'edit_blocklist' | 'choose_blocklist' | null} * 'new_blocklist' | 'edit_blocklist' | 'choose_blocklist' | null}
*/ */
const getTitle = (modalType: any, whitelist: any) => { const getTitle = (modalType, whitelist) => {
const titleType = MODAL_TYPE_TO_TITLE_TYPE_MAP[modalType]; const titleType = MODAL_TYPE_TO_TITLE_TYPE_MAP[modalType];
if (!titleType) { if (!titleType) {
return null; return null;
@@ -35,39 +33,19 @@ const getTitle = (modalType: any, whitelist: any) => {
return `${titleType}_${whitelist ? 'allowlist' : 'blocklist'}`; return `${titleType}_${whitelist ? 'allowlist' : 'blocklist'}`;
}; };
const getSelectedValues = (filters: any, catalogSourcesToIdMap: any) => const getSelectedValues = (filters, catalogSourcesToIdMap) => filters.reduce((acc, { url }) => {
filters.reduce( if (Object.prototype.hasOwnProperty.call(catalogSourcesToIdMap, url)) {
(acc: any, { url }: any) => { const fieldId = `filter${catalogSourcesToIdMap[url]}`;
if (Object.prototype.hasOwnProperty.call(catalogSourcesToIdMap, url)) { acc.selectedFilterIds[fieldId] = true;
const fieldId = `filter${catalogSourcesToIdMap[url]}`; acc.selectedSources[url] = true;
acc.selectedFilterIds[fieldId] = true; }
acc.selectedSources[url] = true; return acc;
} }, {
return acc; selectedFilterIds: {},
}, selectedSources: {},
{ });
selectedFilterIds: {},
selectedSources: {},
},
);
interface ModalProps { class Modal extends Component {
toggleFilteringModal: (...args: unknown[]) => unknown;
isOpen: boolean;
addFilter: (...args: unknown[]) => unknown;
isFilterAdded: boolean;
processingAddFilter: boolean;
processingConfigFilter: boolean;
handleSubmit: (values: any) => void;
modalType: string;
currentFilterData: object;
t: (...args: unknown[]) => string;
whitelist?: boolean;
filters: unknown[];
filtersCatalog?: any;
}
class Modal extends Component<ModalProps> {
closeModal = () => { closeModal = () => {
this.props.toggleFilteringModal(); this.props.toggleFilteringModal();
}; };
@@ -75,25 +53,15 @@ class Modal extends Component<ModalProps> {
render() { render() {
const { const {
isOpen, isOpen,
processingAddFilter, processingAddFilter,
processingConfigFilter, processingConfigFilter,
handleSubmit, handleSubmit,
modalType, modalType,
currentFilterData, currentFilterData,
whitelist, whitelist,
toggleFilteringModal, toggleFilteringModal,
filters, filters,
t, t,
filtersCatalog, filtersCatalog,
} = this.props; } = this.props;
@@ -122,16 +90,15 @@ class Modal extends Component<ModalProps> {
className="Modal__Bootstrap modal-dialog modal-dialog-centered" className="Modal__Bootstrap modal-dialog modal-dialog-centered"
closeTimeoutMS={0} closeTimeoutMS={0}
isOpen={isOpen} isOpen={isOpen}
onRequestClose={this.closeModal}> onRequestClose={this.closeModal}
>
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
{title && <h4 className="modal-title">{title}</h4>} {title && <h4 className="modal-title">{title}</h4>}
<button type="button" className="close" onClick={this.closeModal}> <button type="button" className="close" onClick={this.closeModal}>
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</button> </button>
</div> </div>
<Form <Form
selectedSources={selectedSources} selectedSources={selectedSources}
initialValues={initialValues} initialValues={initialValues}
@@ -149,4 +116,20 @@ class Modal extends Component<ModalProps> {
} }
} }
Modal.propTypes = {
toggleFilteringModal: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
addFilter: PropTypes.func.isRequired,
isFilterAdded: PropTypes.bool.isRequired,
processingAddFilter: PropTypes.bool.isRequired,
processingConfigFilter: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
modalType: PropTypes.string.isRequired,
currentFilterData: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
whitelist: PropTypes.bool,
filters: PropTypes.array.isRequired,
filtersCatalog: PropTypes.object,
};
export default withTranslation()(Modal); export default withTranslation()(Modal);

View File

@@ -1,26 +1,22 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form'; import { Field, reduxForm } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import { renderInputField } from '../../../helpers/form'; import { renderInputField } from '../../../helpers/form';
import { validateAnswer, validateDomain, validateRequiredValue } from '../../../helpers/validators'; import { validateAnswer, validateDomain, validateRequiredValue } from '../../../helpers/validators';
import { FORM_NAME } from '../../../helpers/constants'; import { FORM_NAME } from '../../../helpers/constants';
interface FormProps { const Form = (props) => {
pristine: boolean; const {
handleSubmit: (...args: unknown[]) => string; t,
reset: (...args: unknown[]) => string; handleSubmit,
toggleRewritesModal: (...args: unknown[]) => unknown; reset,
submitting: boolean; pristine,
processingAdd: boolean; submitting,
t: (...args: unknown[]) => string; toggleRewritesModal,
initialValues?: object; processingAdd,
} } = props;
const Form = (props: FormProps) => {
const { t, handleSubmit, reset, pristine, submitting, toggleRewritesModal, processingAdd } = props;
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@@ -39,19 +35,22 @@ const Form = (props: FormProps) => {
validate={[validateRequiredValue, validateDomain]} validate={[validateRequiredValue, validateDomain]}
/> />
</div> </div>
<Trans>examples_title</Trans>: <Trans>examples_title</Trans>:
<ol className="leading-loose"> <ol className="leading-loose">
<li> <li>
<code>example.org</code> <Trans>example_rewrite_domain</Trans> <code>example.org</code> <Trans>example_rewrite_domain</Trans>
</li> </li>
<li> <li>
<code>*.example.org</code> &nbsp; <code>*.example.org</code> &nbsp;
<span> <span>
<Trans components={[<code key="0">text</code>]}>example_rewrite_wildcard</Trans> <Trans components={[<code key="0">text</code>]}>
example_rewrite_wildcard
</Trans>
</span> </span>
</li> </li>
</ol> </ol>
<div className="form__group"> <div className="form__group">
<Field <Field
id="answer" id="answer"
@@ -64,15 +63,14 @@ const Form = (props: FormProps) => {
/> />
</div> </div>
</div> </div>
<ul>{['rewrite_ip_address',
<ul> 'rewrite_domain_name',
{['rewrite_ip_address', 'rewrite_domain_name', 'rewrite_A', 'rewrite_AAAA'].map((str) => ( 'rewrite_A',
<li key={str}> 'rewrite_AAAA']
<Trans components={[<code key="0">text</code>]}>{str}</Trans> .map((str) => <li key={str}>
</li> <Trans components={[<code key="0">text</code>]}>{str}</Trans>
))} </li>)
</ul> }</ul>
<div className="modal-footer"> <div className="modal-footer">
<div className="btn-list"> <div className="btn-list">
<button <button
@@ -82,14 +80,15 @@ const Form = (props: FormProps) => {
onClick={() => { onClick={() => {
reset(); reset();
toggleRewritesModal(); toggleRewritesModal();
}}> }}
>
<Trans>cancel_btn</Trans> <Trans>cancel_btn</Trans>
</button> </button>
<button <button
type="submit" type="submit"
className="btn btn-success btn-standard" className="btn btn-success btn-standard"
disabled={submitting || pristine || processingAdd}> disabled={submitting || pristine || processingAdd}
>
<Trans>save_btn</Trans> <Trans>save_btn</Trans>
</button> </button>
</div> </div>
@@ -98,6 +97,17 @@ const Form = (props: FormProps) => {
); );
}; };
Form.propTypes = {
pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
toggleRewritesModal: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
processingAdd: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
initialValues: PropTypes.object,
};
export default flow([ export default flow([
withTranslation(), withTranslation(),
reduxForm({ reduxForm({

View File

@@ -1,23 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import { MODAL_TYPE } from '../../../helpers/constants'; import { MODAL_TYPE } from '../../../helpers/constants';
import Form from './Form'; import Form from './Form';
interface ModalProps { const Modal = (props) => {
isModalOpen: boolean;
handleSubmit: (values: any) => void;
toggleRewritesModal: (...args: unknown[]) => unknown;
processingAdd: boolean;
processingDelete: boolean;
modalType: string;
currentRewrite?: object;
}
const Modal = (props: ModalProps) => {
const { const {
isModalOpen, isModalOpen,
handleSubmit, handleSubmit,
@@ -33,7 +22,8 @@ const Modal = (props: ModalProps) => {
className="Modal__Bootstrap modal-dialog modal-dialog-centered" className="Modal__Bootstrap modal-dialog modal-dialog-centered"
closeTimeoutMS={0} closeTimeoutMS={0}
isOpen={isModalOpen} isOpen={isModalOpen}
onRequestClose={() => toggleRewritesModal()}> onRequestClose={() => toggleRewritesModal()}
>
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h4 className="modal-title"> <h4 className="modal-title">
@@ -43,12 +33,10 @@ const Modal = (props: ModalProps) => {
<Trans>rewrite_add</Trans> <Trans>rewrite_add</Trans>
)} )}
</h4> </h4>
<button type="button" className="close" onClick={() => toggleRewritesModal()}> <button type="button" className="close" onClick={() => toggleRewritesModal()}>
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</button> </button>
</div> </div>
<Form <Form
initialValues={{ ...currentRewrite }} initialValues={{ ...currentRewrite }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
@@ -61,4 +49,14 @@ const Modal = (props: ModalProps) => {
); );
}; };
Modal.propTypes = {
isModalOpen: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
toggleRewritesModal: PropTypes.func.isRequired,
processingAdd: PropTypes.bool.isRequired,
processingDelete: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
currentRewrite: PropTypes.object,
};
export default withTranslation()(Modal); export default withTranslation()(Modal);

View File

@@ -1,26 +1,13 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { sortIp } from '../../../helpers/helpers'; import { sortIp } from '../../../helpers/helpers';
import { MODAL_TYPE, TABLES_MIN_ROWS } from '../../../helpers/constants'; import { MODAL_TYPE, TABLES_MIN_ROWS } from '../../../helpers/constants';
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper'; import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
interface TableProps { class Table extends Component {
t: (...args: unknown[]) => string; cellWrap = ({ value }) => (
list: unknown[];
processing: boolean;
processingAdd: boolean;
processingDelete: boolean;
processingUpdate: boolean;
handleDelete: (...args: unknown[]) => unknown;
toggleRewritesModal: (...args: unknown[]) => unknown;
}
class Table extends Component<TableProps> {
cellWrap = ({ value }: any) => (
<div className="logs__row o-hidden"> <div className="logs__row o-hidden">
<span className="logs__text" title={value}> <span className="logs__text" title={value}>
{value} {value}
@@ -46,7 +33,7 @@ class Table extends Component<TableProps> {
maxWidth: 100, maxWidth: 100,
sortable: false, sortable: false,
resizable: false, resizable: false,
Cell: (value: any) => { Cell: (value) => {
const currentRewrite = { const currentRewrite = {
answer: value.row.answer, answer: value.row.answer,
domain: value.row.domain, domain: value.row.domain,
@@ -64,7 +51,8 @@ class Table extends Component<TableProps> {
}); });
}} }}
disabled={this.props.processingUpdate} disabled={this.props.processingUpdate}
title={this.props.t('edit_table_action')}> title={this.props.t('edit_table_action')}
>
<svg className="icons icon12"> <svg className="icons icon12">
<use xlinkHref="#edit" /> <use xlinkHref="#edit" />
</svg> </svg>
@@ -74,7 +62,8 @@ class Table extends Component<TableProps> {
type="button" type="button"
className="btn btn-icon btn-outline-secondary btn-sm" className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => this.props.handleDelete(currentRewrite)} onClick={() => this.props.handleDelete(currentRewrite)}
title={this.props.t('delete_table_action')}> title={this.props.t('delete_table_action')}
>
<svg className="icons"> <svg className="icons">
<use xlinkHref="#delete" /> <use xlinkHref="#delete" />
</svg> </svg>
@@ -86,7 +75,9 @@ class Table extends Component<TableProps> {
]; ];
render() { render() {
const { t, list, processing, processingAdd, processingDelete } = this.props; const {
t, list, processing, processingAdd, processingDelete,
} = this.props;
return ( return (
<ReactTable <ReactTable
@@ -96,9 +87,7 @@ class Table extends Component<TableProps> {
className="-striped -highlight card-table-overflow" className="-striped -highlight card-table-overflow"
showPagination showPagination
defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE) || 10} defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE) || 10}
onPageSizeChange={(size: any) => onPageSizeChange={(size) => LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE, size)}
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE, size)
}
minRows={TABLES_MIN_ROWS} minRows={TABLES_MIN_ROWS}
ofText="/" ofText="/"
previousText={t('previous_btn')} previousText={t('previous_btn')}
@@ -112,4 +101,15 @@ class Table extends Component<TableProps> {
} }
} }
Table.propTypes = {
t: PropTypes.func.isRequired,
list: PropTypes.array.isRequired,
processing: PropTypes.bool.isRequired,
processingAdd: PropTypes.bool.isRequired,
processingDelete: PropTypes.bool.isRequired,
processingUpdate: PropTypes.bool.isRequired,
handleDelete: PropTypes.func.isRequired,
toggleRewritesModal: PropTypes.func.isRequired,
};
export default withTranslation()(Table); export default withTranslation()(Table);

View File

@@ -1,39 +1,26 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import Table from './Table'; import Table from './Table';
import Modal from './Modal'; import Modal from './Modal';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import PageTitle from '../../ui/PageTitle'; import PageTitle from '../../ui/PageTitle';
import { MODAL_TYPE } from '../../../helpers/constants'; import { MODAL_TYPE } from '../../../helpers/constants';
import { RewritesData } from '../../../initialState';
interface RewritesProps { class Rewrites extends Component {
t: (...args: unknown[]) => string;
getRewritesList: () => (dispatch: any) => void;
toggleRewritesModal: (...args: unknown[]) => unknown;
addRewrite: (...args: unknown[]) => unknown;
deleteRewrite: (...args: unknown[]) => unknown;
updateRewrite: (...args: unknown[]) => unknown;
rewrites: RewritesData;
}
class Rewrites extends Component<RewritesProps> {
componentDidMount() { componentDidMount() {
this.props.getRewritesList(); this.props.getRewritesList();
} }
handleDelete = (values: any) => { handleDelete = (values) => {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (window.confirm(this.props.t('rewrite_confirm_delete', { key: values.domain }))) { if (window.confirm(this.props.t('rewrite_confirm_delete', { key: values.domain }))) {
this.props.deleteRewrite(values); this.props.deleteRewrite(values);
} }
}; };
handleSubmit = (values: any) => { handleSubmit = (values) => {
const { modalType, currentRewrite } = this.props.rewrites; const { modalType, currentRewrite } = this.props.rewrites;
if (modalType === MODAL_TYPE.EDIT_REWRITE && currentRewrite) { if (modalType === MODAL_TYPE.EDIT_REWRITE && currentRewrite) {
@@ -49,9 +36,7 @@ class Rewrites extends Component<RewritesProps> {
render() { render() {
const { const {
t, t,
rewrites, rewrites,
toggleRewritesModal, toggleRewritesModal,
} = this.props; } = this.props;
@@ -68,9 +53,14 @@ class Rewrites extends Component<RewritesProps> {
return ( return (
<Fragment> <Fragment>
<PageTitle title={t('dns_rewrites')} subtitle={t('rewrite_desc')} /> <PageTitle
title={t('dns_rewrites')}
<Card id="rewrites" bodyType="card-body box-body--settings"> subtitle={t('rewrite_desc')}
/>
<Card
id="rewrites"
bodyType="card-body box-body--settings"
>
<Fragment> <Fragment>
<Table <Table
list={list} list={list}
@@ -86,7 +76,8 @@ class Rewrites extends Component<RewritesProps> {
type="button" type="button"
className="btn btn-success btn-standard mt-3" className="btn btn-success btn-standard mt-3"
onClick={() => toggleRewritesModal({ type: MODAL_TYPE.ADD_REWRITE })} onClick={() => toggleRewritesModal({ type: MODAL_TYPE.ADD_REWRITE })}
disabled={processingAdd}> disabled={processingAdd}
>
<Trans>rewrite_add</Trans> <Trans>rewrite_add</Trans>
</button> </button>
@@ -97,6 +88,7 @@ class Rewrites extends Component<RewritesProps> {
handleSubmit={this.handleSubmit} handleSubmit={this.handleSubmit}
processingAdd={processingAdd} processingAdd={processingAdd}
processingDelete={processingDelete} processingDelete={processingDelete}
processingUpdate={processingUpdate}
currentRewrite={currentRewrite} currentRewrite={currentRewrite}
/> />
</Fragment> </Fragment>
@@ -106,4 +98,14 @@ class Rewrites extends Component<RewritesProps> {
} }
} }
Rewrites.propTypes = {
t: PropTypes.func.isRequired,
getRewritesList: PropTypes.func.isRequired,
toggleRewritesModal: PropTypes.func.isRequired,
addRewrite: PropTypes.func.isRequired,
deleteRewrite: PropTypes.func.isRequired,
updateRewrite: PropTypes.func.isRequired,
rewrites: PropTypes.object.isRequired,
};
export default withTranslation()(Rewrites); export default withTranslation()(Rewrites);

View File

@@ -1,27 +1,23 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form'; import { Field, reduxForm } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import { toggleAllServices } from '../../../helpers/helpers'; import { toggleAllServices } from '../../../helpers/helpers';
import { renderServiceField } from '../../../helpers/form'; import { renderServiceField } from '../../../helpers/form';
import { FORM_NAME } from '../../../helpers/constants'; import { FORM_NAME } from '../../../helpers/constants';
interface FormProps { const Form = (props) => {
blockedServices: unknown[]; const {
pristine: boolean; blockedServices,
handleSubmit: (...args: unknown[]) => string; handleSubmit,
change: (...args: unknown[]) => unknown; change,
submitting: boolean; pristine,
processing: boolean; submitting,
processingSet: boolean; processing,
t: (...args: unknown[]) => string; processingSet,
} } = props;
const Form = (props: FormProps) => {
const { blockedServices, handleSubmit, change, pristine, submitting, processing, processingSet } = props;
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@@ -32,24 +28,24 @@ const Form = (props: FormProps) => {
type="button" type="button"
className="btn btn-secondary btn-block" className="btn btn-secondary btn-block"
disabled={processing || processingSet} disabled={processing || processingSet}
onClick={() => toggleAllServices(blockedServices, change, true)}> onClick={() => toggleAllServices(blockedServices, change, true)}
>
<Trans>block_all</Trans> <Trans>block_all</Trans>
</button> </button>
</div> </div>
<div className="col-6"> <div className="col-6">
<button <button
type="button" type="button"
className="btn btn-secondary btn-block" className="btn btn-secondary btn-block"
disabled={processing || processingSet} disabled={processing || processingSet}
onClick={() => toggleAllServices(blockedServices, change, false)}> onClick={() => toggleAllServices(blockedServices, change, false)}
>
<Trans>unblock_all</Trans> <Trans>unblock_all</Trans>
</button> </button>
</div> </div>
</div> </div>
<div className="services"> <div className="services">
{blockedServices.map((service: any) => ( {blockedServices.map((service) => (
<Field <Field
key={service.id} key={service.id}
icon={service.icon_svg} icon={service.icon_svg}
@@ -67,7 +63,8 @@ const Form = (props: FormProps) => {
<button <button
type="submit" type="submit"
className="btn btn-success btn-standard btn-large" className="btn btn-success btn-standard btn-large"
disabled={submitting || pristine || processing || processingSet}> disabled={submitting || pristine || processing || processingSet}
>
<Trans>save_btn</Trans> <Trans>save_btn</Trans>
</button> </button>
</div> </div>
@@ -75,6 +72,17 @@ const Form = (props: FormProps) => {
); );
}; };
Form.propTypes = {
blockedServices: PropTypes.array.isRequired,
pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
processingSet: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default flow([ export default flow([
withTranslation(), withTranslation(),
reduxForm({ reduxForm({

View File

@@ -1,12 +1,10 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import { Timezone } from './Timezone'; import { Timezone } from './Timezone';
import { TimeSelect } from './TimeSelect'; import { TimeSelect } from './TimeSelect';
import { TimePeriod } from './TimePeriod'; import { TimePeriod } from './TimePeriod';
import { getFullDayName, getShortDayName } from './helpers'; import { getFullDayName, getShortDayName } from './helpers';
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants'; import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
@@ -16,26 +14,21 @@ export const DAYS_OF_WEEK = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const INITIAL_START_TIME_MS = 0; const INITIAL_START_TIME_MS = 0;
const INITIAL_END_TIME_MS = 86340000; const INITIAL_END_TIME_MS = 86340000;
interface ModalProps { export const Modal = ({
schedule: { isOpen,
time_zone: string; currentDay,
}; schedule,
currentDay?: string; onClose,
isOpen: boolean; onSubmit,
onClose: (...args: unknown[]) => unknown; }) => {
onSubmit: (values: any) => void;
}
export const Modal = ({ isOpen, currentDay, schedule, onClose, onSubmit }: ModalProps) => {
const [t] = useTranslation(); const [t] = useTranslation();
const intialTimezone = const intialTimezone = schedule.time_zone === LOCAL_TIMEZONE_VALUE
schedule.time_zone === LOCAL_TIMEZONE_VALUE ? Intl.DateTimeFormat().resolvedOptions().timeZone
? Intl.DateTimeFormat().resolvedOptions().timeZone : schedule.time_zone;
: schedule.time_zone;
const [timezone, setTimezone] = useState(intialTimezone); const [timezone, setTimezone] = useState(intialTimezone);
const [days, setDays] = useState<Set<string>>(new Set()); const [days, setDays] = useState(new Set());
const [startTime, setStartTime] = useState(INITIAL_START_TIME_MS); const [startTime, setStartTime] = useState(INITIAL_START_TIME_MS);
const [endTime, setEndTime] = useState(INITIAL_END_TIME_MS); const [endTime, setEndTime] = useState(INITIAL_END_TIME_MS);
@@ -60,7 +53,7 @@ export const Modal = ({ isOpen, currentDay, schedule, onClose, onSubmit }: Modal
} }
}, [startTime, endTime]); }, [startTime, endTime]);
const addDays = (day: any) => { const addDays = (day) => {
const newDays = new Set(days); const newDays = new Set(days);
if (newDays.has(day)) { if (newDays.has(day)) {
@@ -72,11 +65,11 @@ export const Modal = ({ isOpen, currentDay, schedule, onClose, onSubmit }: Modal
setDays(newDays); setDays(newDays);
}; };
const activeDay = (day: any) => { const activeDay = (day) => {
return days.has(day); return days.has(day);
}; };
const onFormSubmit = (e: any) => { const onFormSubmit = (e) => {
e.preventDefault(); e.preventDefault();
const newSchedule = schedule; const newSchedule = schedule;
@@ -100,19 +93,23 @@ export const Modal = ({ isOpen, currentDay, schedule, onClose, onSubmit }: Modal
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--schedule" className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--schedule"
closeTimeoutMS={0} closeTimeoutMS={0}
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onClose}> onRequestClose={onClose}
>
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h4 className="modal-title">{currentDay ? t('schedule_edit') : t('schedule_new')}</h4> <h4 className="modal-title">
{currentDay ? t('schedule_edit') : t('schedule_new')}
</h4>
<button type="button" className="close" onClick={onClose}> <button type="button" className="close" onClick={onClose}>
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</button> </button>
</div> </div>
<form onSubmit={onFormSubmit}> <form onSubmit={onFormSubmit}>
<div className="modal-body"> <div className="modal-body">
<Timezone timezone={timezone} setTimezone={setTimezone} /> <Timezone
timezone={timezone}
setTimezone={setTimezone}
/>
<div className="schedule__days"> <div className="schedule__days">
{DAYS_OF_WEEK.map((day) => ( {DAYS_OF_WEEK.map((day) => (
@@ -121,7 +118,8 @@ export const Modal = ({ isOpen, currentDay, schedule, onClose, onSubmit }: Modal
key={day} key={day}
className="btn schedule__button-day" className="btn schedule__button-day"
data-active={activeDay(day)} data-active={activeDay(day)}
onClick={() => addDays(day)}> onClick={() => addDays(day)}
>
{getShortDayName(t, day)} {getShortDayName(t, day)}
</button> </button>
))} ))}
@@ -129,52 +127,69 @@ export const Modal = ({ isOpen, currentDay, schedule, onClose, onSubmit }: Modal
<div className="schedule__time-wrap"> <div className="schedule__time-wrap">
<div className="schedule__time-row"> <div className="schedule__time-row">
<TimeSelect value={startTime} onChange={(v) => setStartTime(v)} /> <TimeSelect
value={startTime}
onChange={(v) => setStartTime(v)}
/>
<TimeSelect value={endTime} onChange={(v) => setEndTime(v)} /> <TimeSelect
value={endTime}
onChange={(v) => setEndTime(v)}
/>
</div> </div>
{wrongPeriod && <div className="schedule__error">{t('schedule_invalid_select')}</div>} {wrongPeriod && (
<div className="schedule__error">
{t('schedule_invalid_select')}
</div>
)}
</div> </div>
<div className="schedule__info"> <div className="schedule__info">
<div className="schedule__info-title">{t('schedule_modal_time_off')}</div> <div className="schedule__info-title">
{t('schedule_modal_time_off')}
</div>
<div className="schedule__info-row"> <div className="schedule__info-row">
<svg className="icons schedule__info-icon"> <svg className="icons schedule__info-icon">
<use xlinkHref="#calendar" /> <use xlinkHref="#calendar" />
</svg> </svg>
{days.size ? ( {days.size ? (
Array.from(days) Array.from(days).map((day) => getFullDayName(t, day)).join(', ')
.map((day) => getFullDayName(t, day))
.join(', ')
) : ( ) : (
<span></span> <span>
</span>
)} )}
</div> </div>
<div className="schedule__info-row"> <div className="schedule__info-row">
<svg className="icons schedule__info-icon"> <svg className="icons schedule__info-icon">
<use xlinkHref="#watch" /> <use xlinkHref="#watch" />
</svg> </svg>
{wrongPeriod ? ( {wrongPeriod ? (
<span></span> <span>
</span>
) : ( ) : (
<TimePeriod startTimeMs={startTime} endTimeMs={endTime} /> <TimePeriod
startTimeMs={startTime}
endTimeMs={endTime}
/>
)} )}
</div> </div>
</div> </div>
<div className="schedule__notice">{t('schedule_modal_description')}</div> <div className="schedule__notice">
{t('schedule_modal_description')}
</div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<div className="btn-list"> <div className="btn-list">
<button <button
type="button" type="button"
className="btn btn-success btn-standard" className="btn btn-success btn-standard"
disabled={days.size === 0 || wrongPeriod} disabled={days.size === 0 || wrongPeriod}
onClick={onFormSubmit}> onClick={onFormSubmit}
>
{currentDay ? t('schedule_save') : t('schedule_add')} {currentDay ? t('schedule_save') : t('schedule_add')}
</button> </button>
</div> </div>
@@ -184,3 +199,11 @@ export const Modal = ({ isOpen, currentDay, schedule, onClose, onSubmit }: Modal
</ReactModal> </ReactModal>
); );
}; };
Modal.propTypes = {
schedule: PropTypes.object.isRequired,
currentDay: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getTimeFromMs } from './helpers';
export const TimePeriod = ({
startTimeMs,
endTimeMs,
}) => {
const startTime = getTimeFromMs(startTimeMs);
const endTime = getTimeFromMs(endTimeMs);
return (
<div className="schedule__time">
<time>{startTime.hours}:{startTime.minutes}</time>
&nbsp;&nbsp;
<time>{endTime.hours}:{endTime.minutes}</time>
</div>
);
};
TimePeriod.propTypes = {
startTimeMs: PropTypes.number.isRequired,
endTimeMs: PropTypes.number.isRequired,
};

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { getTimeFromMs } from './helpers';
interface TimePeriodProps {
startTimeMs: number;
endTimeMs: number;
}
export const TimePeriod = ({ startTimeMs, endTimeMs }: TimePeriodProps) => {
const startTime = getTimeFromMs(startTimeMs);
const endTime = getTimeFromMs(endTimeMs);
return (
<div className="schedule__time">
<time>
{startTime.hours}:{startTime.minutes}
</time>
&nbsp;&nbsp;
<time>
{endTime.hours}:{endTime.minutes}
</time>
</div>
);
};

View File

@@ -1,35 +1,37 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { getTimeFromMs, convertTimeToMs } from './helpers'; import { getTimeFromMs, convertTimeToMs } from './helpers';
interface TimeSelectProps { export const TimeSelect = ({
value: number; value,
onChange: (time: number) => void; onChange,
} }) => {
export const TimeSelect = ({ value, onChange }: TimeSelectProps) => {
const { hours: initialHours, minutes: initialMinutes } = getTimeFromMs(value); const { hours: initialHours, minutes: initialMinutes } = getTimeFromMs(value);
const [hours, setHours] = useState(initialHours); const [hours, setHours] = useState(initialHours);
const [minutes, setMinutes] = useState(initialMinutes); const [minutes, setMinutes] = useState(initialMinutes);
const hourOptions = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); const hourOptions = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0')); const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
const onHourChange = (event: any) => { const onHourChange = (event) => {
setHours(event.target.value); setHours(event.target.value);
onChange(convertTimeToMs(event.target.value, minutes)); onChange(convertTimeToMs(event.target.value, minutes));
}; };
const onMinuteChange = (event: any) => { const onMinuteChange = (event) => {
setMinutes(event.target.value); setMinutes(event.target.value);
onChange(convertTimeToMs(hours, event.target.value)); onChange(convertTimeToMs(hours, event.target.value));
}; };
return ( return (
<div className="schedule__time-select"> <div className="schedule__time-select">
<select value={hours} onChange={onHourChange} className="form-control custom-select"> <select
value={hours}
onChange={onHourChange}
className="form-control custom-select"
>
{hourOptions.map((hour) => ( {hourOptions.map((hour) => (
<option key={hour} value={hour}> <option key={hour} value={hour}>
{hour} {hour}
@@ -37,7 +39,11 @@ export const TimeSelect = ({ value, onChange }: TimeSelectProps) => {
))} ))}
</select> </select>
&nbsp;:&nbsp; &nbsp;:&nbsp;
<select value={minutes} onChange={onMinuteChange} className="form-control custom-select"> <select
value={minutes}
onChange={onMinuteChange}
className="form-control custom-select"
>
{minuteOptions.map((minute) => ( {minuteOptions.map((minute) => (
<option key={minute} value={minute}> <option key={minute} value={minute}>
{minute} {minute}
@@ -47,3 +53,8 @@ export const TimeSelect = ({ value, onChange }: TimeSelectProps) => {
</div> </div>
); );
}; };
TimeSelect.propTypes = {
value: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
};

View File

@@ -1,18 +1,17 @@
import React from 'react'; import React from 'react';
import ct from 'countries-and-timezones'; import ct from 'countries-and-timezones';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants'; import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
interface TimezoneProps { export const Timezone = ({
timezone: string; timezone,
setTimezone: (...args: unknown[]) => unknown; setTimezone,
} }) => {
export const Timezone = ({ timezone, setTimezone }: TimezoneProps) => {
const [t] = useTranslation(); const [t] = useTranslation();
const onTimeZoneChange = (event: any) => { const onTimeZoneChange = (event) => {
setTimezone(event.target.value); setTimezone(event.target.value);
}; };
@@ -20,10 +19,18 @@ export const Timezone = ({ timezone, setTimezone }: TimezoneProps) => {
return ( return (
<div className="schedule__timezone"> <div className="schedule__timezone">
<label className="form__label form__label--with-desc mb-2">{t('schedule_timezone')}</label> <label className="form__label form__label--with-desc mb-2">
{t('schedule_timezone')}
</label>
<select className="form-control custom-select" value={timezone} onChange={onTimeZoneChange}> <select
<option value={LOCAL_TIMEZONE_VALUE}>{t('schedule_timezone')}</option> className="form-control custom-select"
value={timezone}
onChange={onTimeZoneChange}
>
<option value={LOCAL_TIMEZONE_VALUE}>
{t('schedule_timezone')}
</option>
{/* TODO: get timezones from backend method when the method is ready */} {/* TODO: get timezones from backend method when the method is ready */}
{Object.keys(timezones).map((zone) => ( {Object.keys(timezones).map((zone) => (
<option key={zone} value={zone}> <option key={zone} value={zone}>
@@ -34,3 +41,8 @@ export const Timezone = ({ timezone, setTimezone }: TimezoneProps) => {
</div> </div>
); );
}; };
Timezone.propTypes = {
timezone: PropTypes.string.isRequired,
setTimezone: PropTypes.func.isRequired,
};

View File

@@ -1,4 +1,4 @@
export const getFullDayName = (t: any, abbreviation: any) => { export const getFullDayName = (t, abbreviation) => {
const dayMap = { const dayMap = {
sun: t('sunday'), sun: t('sunday'),
mon: t('monday'), mon: t('monday'),
@@ -12,7 +12,7 @@ export const getFullDayName = (t: any, abbreviation: any) => {
return dayMap[abbreviation] || ''; return dayMap[abbreviation] || '';
}; };
export const getShortDayName = (t: any, abbreviation: any) => { export const getShortDayName = (t, abbreviation) => {
const dayMap = { const dayMap = {
sun: t('sunday_short'), sun: t('sunday_short'),
mon: t('monday_short'), mon: t('monday_short'),
@@ -26,19 +26,18 @@ export const getShortDayName = (t: any, abbreviation: any) => {
return dayMap[abbreviation] || ''; return dayMap[abbreviation] || '';
}; };
export const getTimeFromMs = (value: any) => { export const getTimeFromMs = (value) => {
const selectedTime = new Date(value); const selectedTime = new Date(value);
const hours = selectedTime.getUTCHours(); const hours = selectedTime.getUTCHours();
const minutes = selectedTime.getUTCMinutes(); const minutes = selectedTime.getUTCMinutes();
return { return {
hours: hours.toString().padStart(2, '0'), hours: hours.toString().padStart(2, '0'),
minutes: minutes.toString().padStart(2, '0'), minutes: minutes.toString().padStart(2, '0'),
}; };
}; };
export const convertTimeToMs = (hours: any, minutes: any) => { export const convertTimeToMs = (hours, minutes) => {
const selectedTime = new Date(0); const selectedTime = new Date(0);
selectedTime.setUTCHours(parseInt(hours, 10)); selectedTime.setUTCHours(parseInt(hours, 10));
selectedTime.setUTCMinutes(parseInt(minutes, 10)); selectedTime.setUTCMinutes(parseInt(minutes, 10));

View File

@@ -1,23 +1,19 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import cn from 'classnames'; import cn from 'classnames';
import { Modal } from './Modal'; import { Modal } from './Modal';
import { getFullDayName, getShortDayName } from './helpers'; import { getFullDayName, getShortDayName } from './helpers';
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants'; import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
import { TimePeriod } from './TimePeriod'; import { TimePeriod } from './TimePeriod';
import './styles.css'; import './styles.css';
interface ScheduleFormProps { export const ScheduleForm = ({
schedule?: { schedule,
time_zone: string; onScheduleSubmit,
}; clientForm,
onScheduleSubmit: (values: any) => void; }) => {
clientForm?: boolean;
}
export const ScheduleForm = ({ schedule, onScheduleSubmit, clientForm }: ScheduleFormProps) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [currentDay, setCurrentDay] = useState(); const [currentDay, setCurrentDay] = useState();
@@ -29,12 +25,12 @@ export const ScheduleForm = ({ schedule, onScheduleSubmit, clientForm }: Schedul
const scheduleMap = new Map(); const scheduleMap = new Map();
filteredScheduleKeys.forEach((day) => scheduleMap.set(day, schedule[day])); filteredScheduleKeys.forEach((day) => scheduleMap.set(day, schedule[day]));
const onSubmit = (values: any) => { const onSubmit = (values) => {
onScheduleSubmit(values); onScheduleSubmit(values);
onModalClose(); onModalClose();
}; };
const onDelete = (day: any) => { const onDelete = (day) => {
scheduleMap.delete(day); scheduleMap.delete(day);
const scheduleWeek = Object.fromEntries(Array.from(scheduleMap.entries())); const scheduleWeek = Object.fromEntries(Array.from(scheduleMap.entries()));
@@ -45,7 +41,7 @@ export const ScheduleForm = ({ schedule, onScheduleSubmit, clientForm }: Schedul
}); });
}; };
const onEdit = (day: any) => { const onEdit = (day) => {
setCurrentDay(day); setCurrentDay(day);
onModalOpen(); onModalOpen();
}; };
@@ -71,18 +67,23 @@ export const ScheduleForm = ({ schedule, onScheduleSubmit, clientForm }: Schedul
return ( return (
<div key={day} className="schedule__row"> <div key={day} className="schedule__row">
<div className="schedule__day">{getFullDayName(t, day)}</div> <div className="schedule__day">
{getFullDayName(t, day)}
<div className="schedule__day schedule__day--mobile">{getShortDayName(t, day)}</div> </div>
<div className="schedule__day schedule__day--mobile">
<TimePeriod startTimeMs={data.start} endTimeMs={data.end} /> {getShortDayName(t, day)}
</div>
<TimePeriod
startTimeMs={data.start}
endTimeMs={data.end}
/>
<div className="schedule__actions"> <div className="schedule__actions">
<button <button
type="button" type="button"
className="btn btn-icon btn-outline-primary btn-sm schedule__button" className="btn btn-icon btn-outline-primary btn-sm schedule__button"
title={t('edit_table_action')} title={t('edit_table_action')}
onClick={() => onEdit(day)}> onClick={() => onEdit(day)}
>
<svg className="icons icon12"> <svg className="icons icon12">
<use xlinkHref="#edit" /> <use xlinkHref="#edit" />
</svg> </svg>
@@ -92,7 +93,8 @@ export const ScheduleForm = ({ schedule, onScheduleSubmit, clientForm }: Schedul
type="button" type="button"
className="btn btn-icon btn-outline-secondary btn-sm schedule__button" className="btn btn-icon btn-outline-secondary btn-sm schedule__button"
title={t('delete_table_action')} title={t('delete_table_action')}
onClick={() => onDelete(day)}> onClick={() => onDelete(day)}
>
<svg className="icons"> <svg className="icons">
<use xlinkHref="#delete" /> <use xlinkHref="#delete" />
</svg> </svg>
@@ -110,7 +112,8 @@ export const ScheduleForm = ({ schedule, onScheduleSubmit, clientForm }: Schedul
{ 'btn-outline-success btn-sm': clientForm }, { 'btn-outline-success btn-sm': clientForm },
{ 'btn-success btn-standard': !clientForm }, { 'btn-success btn-standard': !clientForm },
)} )}
onClick={onAdd}> onClick={onAdd}
>
{t('schedule_new')} {t('schedule_new')}
</button> </button>
@@ -126,3 +129,9 @@ export const ScheduleForm = ({ schedule, onScheduleSubmit, clientForm }: Schedul
</div> </div>
); );
}; };
ScheduleForm.propTypes = {
schedule: PropTypes.object,
onScheduleSubmit: PropTypes.func.isRequired,
clientForm: PropTypes.bool,
};

View File

@@ -73,7 +73,7 @@
outline: 0; outline: 0;
} }
.schedule__button-day[data-active='true'] { .schedule__button-day[data-active="true"] {
color: var(--btn-success-bgcolor); color: var(--btn-success-bgcolor);
border-color: var(--btn-success-bgcolor); border-color: var(--btn-success-bgcolor);
} }

View File

@@ -2,63 +2,50 @@ import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import Form from './Form'; import Form from './Form';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services'; import { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services';
import PageTitle from '../../ui/PageTitle'; import PageTitle from '../../ui/PageTitle';
import { ScheduleForm } from './ScheduleForm'; import { ScheduleForm } from './ScheduleForm';
import { RootState } from '../../../initialState';
const getInitialDataForServices = (initial: any) => const getInitialDataForServices = (initial) => (initial ? initial.reduce(
initial (acc, service) => {
? initial.reduce( acc.blocked_services[service] = true;
(acc: any, service: any) => { return acc;
acc.blocked_services[service] = true; }, { blocked_services: {} },
return acc; ) : initial);
},
{ blocked_services: {} },
)
: initial;
const Services = () => { const Services = () => {
const [t] = useTranslation(); const [t] = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const services = useSelector((store) => store?.services);
const services = useSelector((state: RootState) => state.services);
useEffect(() => { useEffect(() => {
dispatch(getBlockedServices()); dispatch(getBlockedServices());
dispatch(getAllBlockedServices()); dispatch(getAllBlockedServices());
}, []); }, []);
const handleSubmit = (values: any) => { const handleSubmit = (values) => {
if (!values || !values.blocked_services) { if (!values || !values.blocked_services) {
return; return;
} }
const blocked_services = Object.keys(values.blocked_services).filter( const blocked_services = Object
(service) => values.blocked_services[service], .keys(values.blocked_services)
); .filter((service) => values.blocked_services[service]);
dispatch( dispatch(updateBlockedServices({
updateBlockedServices({ ids: blocked_services,
ids: blocked_services, schedule: services.list.schedule,
schedule: services.list.schedule, }));
}),
);
}; };
const handleScheduleSubmit = (values: any) => { const handleScheduleSubmit = (values) => {
dispatch( dispatch(updateBlockedServices({
updateBlockedServices({ ids: services.list.ids,
ids: services.list.ids, schedule: values,
schedule: values, }));
}),
);
}; };
const initialValues = getInitialDataForServices(services.list.ids); const initialValues = getInitialDataForServices(services.list.ids);
@@ -69,9 +56,13 @@ const Services = () => {
return ( return (
<> <>
<PageTitle title={t('blocked_services')} subtitle={t('blocked_services_desc')} /> <PageTitle
title={t('blocked_services')}
<Card bodyType="card-body box-body--settings"> subtitle={t('blocked_services_desc')}
/>
<Card
bodyType="card-body box-body--settings"
>
<div className="form"> <div className="form">
<Form <Form
initialValues={initialValues} initialValues={initialValues}
@@ -86,8 +77,12 @@ const Services = () => {
<Card <Card
title={t('schedule_services')} title={t('schedule_services')}
subtitle={t('schedule_services_desc')} subtitle={t('schedule_services_desc')}
bodyType="card-body box-body--settings"> bodyType="card-body box-body--settings"
<ScheduleForm schedule={services.list.schedule} onScheduleSubmit={handleScheduleSubmit} /> >
<ScheduleForm
schedule={services.list.schedule}
onScheduleSubmit={handleScheduleSubmit}
/>
</Card> </Card>
</> </>
); );

View File

@@ -1,32 +1,17 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { withTranslation, Trans } from 'react-i18next'; import { withTranslation, Trans } from 'react-i18next';
import CellWrap from '../ui/CellWrap'; import CellWrap from '../ui/CellWrap';
import { MODAL_TYPE } from '../../helpers/constants'; import { MODAL_TYPE } from '../../helpers/constants';
import { formatDetailedDateTime } from '../../helpers/helpers'; import { formatDetailedDateTime } from '../../helpers/helpers';
import { isValidAbsolutePath } from '../../helpers/form'; import { isValidAbsolutePath } from '../../helpers/form';
import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from '../../helpers/localStorageHelper'; import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from '../../helpers/localStorageHelper';
interface TableProps { class Table extends Component {
filters: unknown[]; getDateCell = (row) => CellWrap(row, formatDetailedDateTime);
loading: boolean;
processingConfigFilter: boolean;
toggleFilteringModal: (...args: unknown[]) => unknown;
handleDelete: (...args: unknown[]) => unknown;
toggleFilter: (...args: unknown[]) => unknown;
t: (...args: unknown[]) => string;
whitelist?: boolean;
}
class Table extends Component<TableProps> { renderCheckbox = ({ original }) => {
getDateCell = (row: any) => CellWrap(row, formatDetailedDateTime);
renderCheckbox = ({ original }: any) => {
const { processingConfigFilter, toggleFilter } = this.props; const { processingConfigFilter, toggleFilter } = this.props;
const { url, name, enabled } = original; const { url, name, enabled } = original;
const data = { name, url, enabled: !enabled }; const data = { name, url, enabled: !enabled };
@@ -40,7 +25,6 @@ class Table extends Component<TableProps> {
checked={enabled} checked={enabled}
disabled={processingConfigFilter} disabled={processingConfigFilter}
/> />
<span className="checkbox__label" /> <span className="checkbox__label" />
</label> </label>
); );
@@ -66,15 +50,17 @@ class Table extends Component<TableProps> {
accessor: 'url', accessor: 'url',
minWidth: 180, minWidth: 180,
// eslint-disable-next-line react/prop-types // eslint-disable-next-line react/prop-types
Cell: ({ value }: any) => ( Cell: ({ value }) => (
<div className="logs__row"> <div className="logs__row">
{isValidAbsolutePath(value) ? ( {isValidAbsolutePath(value) ? value
value : <a
) : ( href={value}
<a href={value} target="_blank" rel="noopener noreferrer" className="link logs__text"> target="_blank"
rel="noopener noreferrer"
className="link logs__text"
>
{value} {value}
</a> </a>}
)}
</div> </div>
), ),
}, },
@@ -83,7 +69,7 @@ class Table extends Component<TableProps> {
accessor: 'rulesCount', accessor: 'rulesCount',
className: 'text-center', className: 'text-center',
minWidth: 100, minWidth: 100,
Cell: (props: any) => props.value.toLocaleString(), Cell: (props) => props.value.toLocaleString(),
}, },
{ {
Header: <Trans>last_time_updated_table_header</Trans>, Header: <Trans>last_time_updated_table_header</Trans>,
@@ -99,10 +85,9 @@ class Table extends Component<TableProps> {
width: 100, width: 100,
sortable: false, sortable: false,
resizable: false, resizable: false,
Cell: (row: any) => { Cell: (row) => {
const { original } = row; const { original } = row;
const { url } = original; const { url } = original;
const { t, toggleFilteringModal, handleDelete } = this.props; const { t, toggleFilteringModal, handleDelete } = this.props;
return ( return (
@@ -111,22 +96,22 @@ class Table extends Component<TableProps> {
type="button" type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2" className="btn btn-icon btn-outline-primary btn-sm mr-2"
title={t('edit_table_action')} title={t('edit_table_action')}
onClick={() => onClick={() => toggleFilteringModal({
toggleFilteringModal({ type: MODAL_TYPE.EDIT_FILTERS,
type: MODAL_TYPE.EDIT_FILTERS, url,
url, })
}) }
}> >
<svg className="icons icon12"> <svg className="icons icon12">
<use xlinkHref="#edit" /> <use xlinkHref="#edit" />
</svg> </svg>
</button> </button>
<button <button
type="button" type="button"
className="btn btn-icon btn-outline-secondary btn-sm" className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => handleDelete(url)} onClick={() => handleDelete(url)}
title={t('delete_table_action')}> title={t('delete_table_action')}
>
<svg className="icons icon12"> <svg className="icons icon12">
<use xlinkHref="#delete" /> <use xlinkHref="#delete" />
</svg> </svg>
@@ -138,7 +123,9 @@ class Table extends Component<TableProps> {
]; ];
render() { render() {
const { loading, filters, t, whitelist } = this.props; const {
loading, filters, t, whitelist,
} = this.props;
const localStorageKey = whitelist const localStorageKey = whitelist
? LOCAL_STORAGE_KEYS.ALLOWLIST_PAGE_SIZE ? LOCAL_STORAGE_KEYS.ALLOWLIST_PAGE_SIZE
@@ -150,7 +137,7 @@ class Table extends Component<TableProps> {
columns={this.columns} columns={this.columns}
showPagination showPagination
defaultPageSize={LocalStorageHelper.getItem(localStorageKey) || 10} defaultPageSize={LocalStorageHelper.getItem(localStorageKey) || 10}
onPageSizeChange={(size: any) => LocalStorageHelper.setItem(localStorageKey, size)} onPageSizeChange={(size) => LocalStorageHelper.setItem(localStorageKey, size)}
loading={loading} loading={loading}
minRows={6} minRows={6}
ofText="/" ofText="/"
@@ -165,4 +152,15 @@ class Table extends Component<TableProps> {
} }
} }
Table.propTypes = {
filters: PropTypes.array.isRequired,
loading: PropTypes.bool.isRequired,
processingConfigFilter: PropTypes.bool.isRequired,
toggleFilteringModal: PropTypes.func.isRequired,
handleDelete: PropTypes.func.isRequired,
toggleFilter: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
whitelist: PropTypes.bool,
};
export default withTranslation()(Table); export default withTranslation()(Table);

View File

@@ -1,12 +1,10 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import enhanceWithClickOutside from 'react-click-outside'; import enhanceWithClickOutside from 'react-click-outside';
import classnames from 'classnames'; import classnames from 'classnames';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants'; import { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants';
import Dropdown from '../ui/Dropdown'; import Dropdown from '../ui/Dropdown';
const MENU_ITEMS = [ const MENU_ITEMS = [
@@ -82,14 +80,7 @@ const FILTERS_ITEMS = [
}, },
]; ];
interface MenuProps { class Menu extends Component {
isMenuOpen: boolean;
closeMenu: (...args: unknown[]) => unknown;
pathname: string;
t?: (...args: unknown[]) => string;
}
class Menu extends Component<MenuProps> {
handleClickOutside = () => { handleClickOutside = () => {
this.props.closeMenu(); this.props.closeMenu();
}; };
@@ -98,51 +89,52 @@ class Menu extends Component<MenuProps> {
this.props.closeMenu(); this.props.closeMenu();
}; };
getActiveClassForDropdown = (URLS: any) => { getActiveClassForDropdown = (URLS) => {
const isActivePage = Object.values(URLS) const isActivePage = Object.values(URLS)
.some((item) => item === this.props.pathname);
.some((item: any) => item === this.props.pathname);
return isActivePage ? 'active' : ''; return isActivePage ? 'active' : '';
}; };
getNavLink = ({ route, exact, text, order, className, icon }: any) => ( getNavLink = ({
route, exact, text, order, className, icon,
}) => (
<NavLink <NavLink
to={route} to={route}
key={route} key={route}
exact={exact || false} exact={exact || false}
className={`order-${order} ${className}`} className={`order-${order} ${className}`}
onClick={this.closeMenu}> onClick={this.closeMenu}
>
{icon && ( {icon && (
<svg className="nav-icon"> <svg className="nav-icon">
<use xlinkHref={`#${icon}`} /> <use xlinkHref={`#${icon}`} />
</svg> </svg>
)} )}
<Trans>{text}</Trans> <Trans>{text}</Trans>
</NavLink> </NavLink>
); );
getDropdown = ({ label, order, URLS, icon, ITEMS }: any) => ( getDropdown = ({
label, order, URLS, icon, ITEMS,
}) => (
<Dropdown <Dropdown
label={this.props.t(label)} label={this.props.t(label)}
baseClassName="dropdown" baseClassName='dropdown'
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`} controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
icon={icon}> icon={icon}>
{ITEMS.map((item: any) => {ITEMS.map((item) => (
this.getNavLink({ this.getNavLink({
...item, ...item,
order, order,
className: 'dropdown-item', className: 'dropdown-item',
}), })))}
)}
</Dropdown> </Dropdown>
); );
render() { render() {
const menuClass = classnames({ const menuClass = classnames({
'header__column mobile-menu': true, 'header__column mobile-menu': true,
'mobile-menu--active': this.props.isMenuOpen, 'mobile-menu--active': this.props.isMenuOpen,
}); });
return ( return (
@@ -150,14 +142,17 @@ class Menu extends Component<MenuProps> {
<div className={menuClass}> <div className={menuClass}>
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap"> <ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
{MENU_ITEMS.map((item) => ( {MENU_ITEMS.map((item) => (
<li className={`nav-item order-${item.order}`} key={item.text} onClick={this.closeMenu}> <li
className={`nav-item order-${item.order}`}
key={item.text}
onClick={this.closeMenu}
>
{this.getNavLink({ {this.getNavLink({
...item, ...item,
className: 'nav-link', className: 'nav-link',
})} })}
</li> </li>
))} ))}
<li className="nav-item order-1"> <li className="nav-item order-1">
{this.getDropdown({ {this.getDropdown({
order: 1, order: 1,
@@ -167,7 +162,6 @@ class Menu extends Component<MenuProps> {
ITEMS: SETTINGS_ITEMS, ITEMS: SETTINGS_ITEMS,
})} })}
</li> </li>
<li className="nav-item order-2"> <li className="nav-item order-2">
{this.getDropdown({ {this.getDropdown({
order: 2, order: 2,
@@ -184,4 +178,11 @@ class Menu extends Component<MenuProps> {
} }
} }
Menu.propTypes = {
isMenuOpen: PropTypes.bool.isRequired,
closeMenu: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
t: PropTypes.func,
};
export default withTranslation()(enhanceWithClickOutside(Menu)); export default withTranslation()(enhanceWithClickOutside(Menu));

View File

@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { shallowEqual, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import Menu from './Menu';
import logo from '../ui/svg/logo.svg';
import './Header.css';
const Header = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { t } = useTranslation();
const {
protectionEnabled,
processing,
isCoreRunning,
processingProfile,
name,
} = useSelector((state) => state.dashboard, shallowEqual);
const { pathname } = useLocation();
const toggleMenuOpen = () => {
setIsMenuOpen((isMenuOpen) => !isMenuOpen);
};
const closeMenu = () => {
setIsMenuOpen(false);
};
const badgeClass = classnames('badge dns-status', {
'badge-success': protectionEnabled,
'badge-danger': !protectionEnabled,
});
return <div className="header">
<div className="header__container">
<div className="header__row">
<div
className="header-toggler d-lg-none ml-lg-0 collapsed"
onClick={toggleMenuOpen}
>
<span className="header-toggler-icon" />
</div>
<div className="header__column">
<div className="d-flex align-items-center">
<Link to="/" className="nav-link pl-0 pr-1">
<img src={logo} alt="AdGuard Home logo" className="header-brand-img" />
</Link>
{!processing && isCoreRunning
&& <span className={badgeClass}
>{t(protectionEnabled ? 'on' : 'off')}
</span>}
</div>
</div>
<Menu
pathname={pathname}
isMenuOpen={isMenuOpen}
closeMenu={closeMenu}
/>
<div className="header__column">
<div className="header__right">
{!processingProfile && name
&& <a href="control/logout" className="btn btn-sm btn-outline-secondary">
{t('sign_out')}
</a>}
</div>
</div>
</div>
</div>
</div>;
};
export default Header;

View File

@@ -1,74 +0,0 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { shallowEqual, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import Menu from './Menu';
import { Logo } from '../ui/svg/logo';
import './Header.css';
import { RootState } from '../../initialState';
const Header = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { t } = useTranslation();
const { protectionEnabled, processing, isCoreRunning, processingProfile, name } = useSelector(
(state: RootState) => state.dashboard,
shallowEqual,
);
const { pathname } = useLocation();
const toggleMenuOpen = () => {
setIsMenuOpen((isMenuOpen) => !isMenuOpen);
};
const closeMenu = () => {
setIsMenuOpen(false);
};
const badgeClass = classnames('badge dns-status', {
'badge-success': protectionEnabled,
'badge-danger': !protectionEnabled,
});
return (
<div className="header">
<div className="header__container">
<div className="header__row">
<div className="header-toggler d-lg-none ml-lg-0 collapsed" onClick={toggleMenuOpen}>
<span className="header-toggler-icon" />
</div>
<div className="header__column">
<div className="d-flex align-items-center">
<Link to="/" className="nav-link pl-0 pr-1">
<Logo className="header-brand-img" />
</Link>
{!processing && isCoreRunning && (
<span className={badgeClass}>{t(protectionEnabled ? 'on' : 'off')}</span>
)}
</div>
</div>
<Menu pathname={pathname} isMenuOpen={isMenuOpen} closeMenu={closeMenu} />
<div className="header__column">
<div className="header__right">
{!processingProfile && name && (
<a href="control/logout" className="btn btn-sm btn-outline-secondary">
{t('sign_out')}
</a>
)}
</div>
</div>
</div>
</div>
</div>
);
};
export default Header;

View File

@@ -1,18 +1,13 @@
import React from 'react'; import React from 'react';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link'; import { HashLink as Link } from 'react-router-hash-link';
const AnonymizerNotification = () => ( const AnonymizerNotification = () => (
<div className="alert alert-primary mt-6"> <div className="alert alert-primary mt-6">
<Trans <Trans components={[
components={[ <strong key="0">text</strong>,
<strong key="0">text</strong>, <Link to="/settings#logs-config" key="1">link</Link>,
]}>
<Link to="/settings#logs-config" key="1">
link
</Link>,
]}>
anonymizer_notification anonymizer_notification
</Trans> </Trans>
</div> </div>

View File

@@ -3,55 +3,36 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import propTypes from 'prop-types';
import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers'; import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers';
import { BLOCK_ACTIONS } from '../../../helpers/constants'; import { BLOCK_ACTIONS } from '../../../helpers/constants';
import { toggleBlocking, toggleBlockingForClient } from '../../../actions'; import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
import IconTooltip from './IconTooltip'; import IconTooltip from './IconTooltip';
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell'; import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
import { toggleClientBlock } from '../../../actions/access'; import { toggleClientBlock } from '../../../actions/access';
import { getBlockClientInfo } from './helpers'; import { getBlockClientInfo } from './helpers';
import { getStats } from '../../../actions/stats'; import { getStats } from '../../../actions/stats';
import { updateLogs } from '../../../actions/queryLogs'; import { updateLogs } from '../../../actions/queryLogs';
import { RootState } from '../../../initialState';
interface ClientCellProps { const ClientCell = ({
client: string; client,
client_id?: string; client_id,
client_info?: { client_info,
name: string; domain,
whois: { reason,
country?: string; }) => {
city?: string;
orgname?: string;
};
disallowed: boolean;
disallowed_rule: string;
};
domain: string;
reason: string;
}
const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCellProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
const autoClients = useSelector((state: RootState) => state.dashboard.autoClients, shallowEqual); const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const allowedClients = useSelector((state: RootState) => state.access.allowed_clients, shallowEqual);
const [isOptionsOpened, setOptionsOpened] = useState(false); const [isOptionsOpened, setOptionsOpened] = useState(false);
const autoClient = autoClients.find((autoClient: any) => autoClient.name === client); const autoClient = autoClients.find((autoClient) => autoClient.name === client);
const clients = useSelector((state) => state.dashboard.clients);
const clients = useSelector((state: RootState) => state.dashboard.clients);
const source = autoClient?.source; const source = autoClient?.source;
const whoisAvailable = client_info && Object.keys(client_info.whois).length > 0; const whoisAvailable = client_info && Object.keys(client_info.whois).length > 0;
const clientName = client_info?.name || client_id; const clientName = client_info?.name || client_id;
@@ -76,7 +57,7 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
const isFiltered = checkFiltered(reason); const isFiltered = checkFiltered(reason);
const clientIds = clients.map((c: any) => c.ids).flat(); const clientIds = clients.map((c) => c.ids).flat();
const nameClass = classNames('w-90 o-hidden d-flex flex-column', { const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
'mt-2': isDetailed && !client_info?.name && !whoisAvailable, 'mt-2': isDetailed && !client_info?.name && !whoisAvailable,
@@ -87,7 +68,7 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
'my-3': isDetailed, 'my-3': isDetailed,
}); });
const renderBlockingButton = (isFiltered: any, domain: any) => { const renderBlockingButton = (isFiltered, domain) => {
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK; const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const { const {
@@ -98,7 +79,7 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
client, client,
client_info?.disallowed || false, client_info?.disallowed || false,
client_info?.disallowed_rule || '', client_info?.disallowed_rule || '',
allowedClients, allowedСlients,
); );
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only'; const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
@@ -127,13 +108,11 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
name: blockingClientKey, name: blockingClientKey,
onClick: async () => { onClick: async () => {
if (window.confirm(confirmMessage)) { if (window.confirm(confirmMessage)) {
await dispatch( await dispatch(toggleClientBlock(
toggleClientBlock( client,
client, client_info?.disallowed || false,
client_info?.disallowed || false, client_info?.disallowed_rule || '',
client_info?.disallowed_rule || '', ));
),
);
await dispatch(updateLogs()); await dispatch(updateLogs());
setOptionsOpened(false); setOptionsOpened(false);
} }
@@ -151,19 +130,21 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
}); });
} }
const getOptions = (options: any) => { const getOptions = (options) => {
if (options.length === 0) { if (options.length === 0) {
return null; return null;
} }
return ( return (
<> <>
{options.map(({ name, onClick, disabled, className }: any) => ( {options.map(({
name, onClick, disabled, className,
}) => (
<button <button
key={name} key={name}
className={classNames('button-action--arrow-option px-4 py-1', className)} className={classNames('button-action--arrow-option px-4 py-1', className)}
onClick={onClick} onClick={onClick}
disabled={disabled}> disabled={disabled}
>
{t(name)} {t(name)}
</button> </button>
))} ))}
@@ -179,7 +160,11 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
return ( return (
<div className={containerClass}> <div className={containerClass}>
<button type="button" className="btn btn-icon btn-sm px-0" onClick={() => setOptionsOpened(true)}> <button
type="button"
className="btn btn-icon btn-sm px-0"
onClick={() => setOptionsOpened(true)}
>
<svg className="icon24 icon--lightgray button-action__icon"> <svg className="icon24 icon--lightgray button-action__icon">
<use xlinkHref="#bullets" /> <use xlinkHref="#bullets" />
</svg> </svg>
@@ -203,7 +188,10 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
}; };
return ( return (
<div className="o-hidden h-100 logs__cell logs__cell--client" role="gridcell"> <div
className="o-hidden h-100 logs__cell logs__cell--client"
role="gridcell"
>
<IconTooltip <IconTooltip
className={hintClass} className={hintClass}
columnClass="grid grid--limited" columnClass="grid grid--limited"
@@ -214,7 +202,6 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
content={processedData} content={processedData}
placement="bottom" placement="bottom"
/> />
<div className={nameClass}> <div className={nameClass}>
<div data-tip={true} data-for={id}> <div data-tip={true} data-for={id}>
{renderFormattedClientCell(client, clientInfo, isDetailed, true)} {renderFormattedClientCell(client, clientInfo, isDetailed, true)}
@@ -223,7 +210,8 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
<Link <Link
className="detailed-info d-none d-sm-block logs__text logs__text--link logs__text--client" className="detailed-info d-none d-sm-block logs__text logs__text--link logs__text--client"
to={`logs?search="${encodeURIComponent(clientName)}"`} to={`logs?search="${encodeURIComponent(clientName)}"`}
title={clientName}> title={clientName}
>
{clientName} {clientName}
</Link> </Link>
)} )}
@@ -233,4 +221,21 @@ const ClientCell = ({ client, client_id, client_info, domain, reason }: ClientCe
); );
}; };
ClientCell.propTypes = {
client: propTypes.string.isRequired,
client_id: propTypes.string,
client_info: propTypes.shape({
name: propTypes.string.isRequired,
whois: propTypes.shape({
country: propTypes.string,
city: propTypes.string,
orgname: propTypes.string,
}).isRequired,
disallowed: propTypes.bool.isRequired,
disallowed_rule: propTypes.string.isRequired,
}),
domain: propTypes.string.isRequired,
reason: propTypes.string.isRequired,
};
export default ClientCell; export default ClientCell;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { useSelector } from 'react-redux';
import propTypes from 'prop-types';
import { formatDateTime, formatTime } from '../../../helpers/helpers';
import { DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_TIME_FORMAT } from '../../../helpers/constants';
const DateCell = ({ time }) => {
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
if (!time) {
return '';
}
const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT);
const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS);
return <div className="logs__cell logs__cell logs__cell--date text-truncate" role="gridcell">
<div className="logs__time" title={formattedTime}>{formattedTime}</div>
{isDetailed
&& <div className="detailed-info d-none d-sm-block text-truncate"
title={formattedDate}>{formattedDate}</div>}
</div>;
};
DateCell.propTypes = {
time: propTypes.string.isRequired,
};
export default DateCell;

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { formatDateTime, formatTime } from '../../../helpers/helpers';
import { DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_TIME_FORMAT } from '../../../helpers/constants';
import { RootState } from '../../../initialState';
interface DateCellProps {
time: string;
}
const DateCell = ({ time }: DateCellProps) => {
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
if (!time) {
return <></>;
}
const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT);
const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS);
return (
<div className="logs__cell logs__cell logs__cell--date text-truncate" role="gridcell">
<div className="logs__time" title={formattedTime}>
{formattedTime}
</div>
{isDetailed && (
<div className="detailed-info d-none d-sm-block text-truncate" title={formattedDate}>
{formattedDate}
</div>
)}
</div>
);
};
export default DateCell;

View File

@@ -1,32 +1,16 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import propTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
LONG_TIME_FORMAT, LONG_TIME_FORMAT,
SCHEME_TO_PROTOCOL_MAP, SCHEME_TO_PROTOCOL_MAP,
} from '../../../helpers/constants'; } from '../../../helpers/constants';
import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers'; import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';
import { getSourceData } from '../../../helpers/trackers/trackers'; import { getSourceData } from '../../../helpers/trackers/trackers';
import IconTooltip from './IconTooltip'; import IconTooltip from './IconTooltip';
import { RootState } from '../../../initialState';
interface DomainCellProps {
answer_dnssec: boolean;
client_proto: string;
domain: string;
unicodeName?: string;
time: string;
type: string;
tracker?: {
name: string;
category: string;
};
ecs?: string;
}
const DomainCell = ({ const DomainCell = ({
answer_dnssec, answer_dnssec,
@@ -37,12 +21,10 @@ const DomainCell = ({
tracker, tracker,
type, type,
ecs, ecs,
}: DomainCellProps) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
const dnssec_enabled = useSelector((state: RootState) => state.dnsConfig.dnssec_enabled); const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const hasTracker = !!tracker; const hasTracker = !!tracker;
@@ -61,15 +43,7 @@ const DomainCell = ({
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || ''; const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
const ip = type ? `${t('type_table_header')}: ${type}` : ''; const ip = type ? `${t('type_table_header')}: ${type}` : '';
let requestDetailsObj: { let requestDetailsObj = {
time_table_header: string;
date: string;
domain: string;
punycode?: string;
ecs?: string;
type_table_header?: string;
protocol?: string;
} = {
time_table_header: formatTime(time, LONG_TIME_FORMAT), time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS), date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
domain, domain,
@@ -102,16 +76,24 @@ const DomainCell = ({
name_table_header: tracker?.name, name_table_header: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category), category_label: hasTracker && captitalizeWords(tracker.category),
source_label: sourceData && ( source_label: sourceData && (
<a href={sourceData.url} target="_blank" rel="noopener noreferrer" className="link--green"> <a
href={sourceData.url}
target="_blank"
rel="noopener noreferrer"
className="link--green"
>
{sourceData.name} {sourceData.name}
</a> </a>
), ),
}; };
const renderGrid = (content: any, idx: any) => { const renderGrid = (content, idx) => {
const preparedContent = typeof content === 'string' ? t(content) : content; const preparedContent = typeof content === 'string' ? t(content) : content;
const className = classNames('text-truncate o-hidden', { 'overflow-break': preparedContent?.length > 100 }); const className = classNames(
'text-truncate o-hidden',
{ 'overflow-break': preparedContent?.length > 100 },
);
return ( return (
<div key={idx} className={className}> <div key={idx} className={className}>
@@ -120,11 +102,10 @@ const DomainCell = ({
); );
}; };
const getGrid = (contentObj: any, title: string, className?: string) => [ const getGrid = (contentObj, title, className) => [
<div key={title} className={classNames('pb-2 grid--title', className)}> <div key={title} className={classNames('pb-2 grid--title', className)}>
{t(title)} {t(title)}
</div>, </div>,
<div key={`${title}-1`} className="grid grid--limited"> <div key={`${title}-1`} className="grid grid--limited">
{React.Children.map(Object.entries(contentObj), renderGrid)} {React.Children.map(Object.entries(contentObj), renderGrid)}
</div>, </div>,
@@ -132,9 +113,7 @@ const DomainCell = ({
const requestDetails = getGrid(requestDetailsObj, 'request_details'); const requestDetails = getGrid(requestDetailsObj, 'request_details');
const renderContent = hasTracker const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails;
? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4'))
: requestDetails;
const valueClass = classNames('w-100 text-truncate', { const valueClass = classNames('w-100 text-truncate', {
'px-2 d-flex justify-content-center flex-column': isDetailed, 'px-2 d-flex justify-content-center flex-column': isDetailed,
@@ -143,7 +122,10 @@ const DomainCell = ({
const details = [ip, protocol].filter(Boolean).join(', '); const details = [ip, protocol].filter(Boolean).join(', ');
return ( return (
<div className="d-flex o-hidden logs__cell logs__cell logs__cell--domain" role="gridcell"> <div
className="d-flex o-hidden logs__cell logs__cell logs__cell--domain"
role="gridcell"
>
{dnssec_enabled && ( {dnssec_enabled && (
<IconTooltip <IconTooltip
className={lockIconClass} className={lockIconClass}
@@ -155,16 +137,14 @@ const DomainCell = ({
placement="bottom" placement="bottom"
/> />
)} )}
<IconTooltip <IconTooltip
className={privacyIconClass} className={privacyIconClass}
tooltipClass="pt-4 pb-5 px-5 mw-75" tooltipClass="pt-4 pb-5 px-5 mw-75"
xlinkHref="privacy" xlinkHref="privacy"
contentItemClass="key-colon" contentItemClass="key-colon"
renderContent={renderContent} renderContent={renderContent}
placement="bottom" place="bottom"
/> />
<div className={valueClass}> <div className={valueClass}>
{unicodeName ? ( {unicodeName ? (
<div className="text-truncate overflow-break-mobile" title={unicodeName}> <div className="text-truncate overflow-break-mobile" title={unicodeName}>
@@ -176,7 +156,10 @@ const DomainCell = ({
</div> </div>
)} )}
{details && isDetailed && ( {details && isDetailed && (
<div className="detailed-info d-none d-sm-block text-truncate" title={details}> <div
className="detailed-info d-none d-sm-block text-truncate"
title={details}
>
{details} {details}
</div> </div>
)} )}
@@ -185,4 +168,15 @@ const DomainCell = ({
); );
}; };
DomainCell.propTypes = {
answer_dnssec: propTypes.bool.isRequired,
client_proto: propTypes.string.isRequired,
domain: propTypes.string.isRequired,
unicodeName: propTypes.string,
time: propTypes.string.isRequired,
type: propTypes.string.isRequired,
tracker: propTypes.object,
ecs: propTypes.string,
};
export default DomainCell; export default DomainCell;

View File

@@ -0,0 +1,54 @@
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames';
import React from 'react';
import { toggleDetailedLogs } from '../../../actions/queryLogs';
import HeaderCell from './HeaderCell';
const Header = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const disableDetailedMode = () => dispatch(toggleDetailedLogs(false));
const enableDetailedMode = () => dispatch(toggleDetailedLogs(true));
const HEADERS = [
{
className: 'logs__cell--date',
content: 'time_table_header',
},
{
className: 'logs__cell--domain',
content: 'request_table_header',
},
{
className: 'logs__cell--response',
content: 'response_table_header',
},
{
className: 'logs__cell--client',
content: <>
{t('client_table_header')}
{<span>
<svg className={classNames('icons icon--24 icon--green cursor--pointer mr-2', { 'icon--selected': !isDetailed })}
onClick={disableDetailedMode}
>
<title>{t('compact')}</title>
<use xlinkHref='#list' /></svg>
<svg className={classNames('icons icon--24 icon--green cursor--pointer', { 'icon--selected': isDetailed })}
onClick={enableDetailedMode}
>
<title>{t('default')}</title>
<use xlinkHref='#detailed_list' />
</svg>
</span>}
</>,
},
];
return <div className="logs__cell--header__container px-5" role="row">
{HEADERS.map(HeaderCell)}
</div>;
};
export default Header;

View File

@@ -1,73 +0,0 @@
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames';
import React from 'react';
import { toggleDetailedLogs } from '../../../actions/queryLogs';
import HeaderCell from './HeaderCell';
import { RootState } from '../../../initialState';
const Header = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const disableDetailedMode = () => dispatch(toggleDetailedLogs(false));
const enableDetailedMode = () => dispatch(toggleDetailedLogs(true));
const HEADERS = [
{
className: 'logs__cell--date',
content: 'time_table_header',
},
{
className: 'logs__cell--domain',
content: 'request_table_header',
},
{
className: 'logs__cell--response',
content: 'response_table_header',
},
{
className: 'logs__cell--client',
content: (
<>
{t('client_table_header')}
{
<span>
<svg
className={classNames('icons icon--24 icon--green cursor--pointer mr-2', {
'icon--selected': !isDetailed,
})}
onClick={disableDetailedMode}>
<title>{t('compact')}</title>
<use xlinkHref="#list" />
</svg>
<svg
className={classNames('icons icon--24 icon--green cursor--pointer', {
'icon--selected': isDetailed,
})}
onClick={enableDetailedMode}>
<title>{t('default')}</title>
<use xlinkHref="#detailed_list" />
</svg>
</span>
}
</>
),
},
];
return (
<div className="logs__cell--header__container px-5" role="row">
{HEADERS.map(HeaderCell)}
</div>
);
};
export default Header;

View File

@@ -0,0 +1,22 @@
import classNames from 'classnames';
import React from 'react';
import propTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
const HeaderCell = ({ content, className }, idx) => {
const { t } = useTranslation();
return <div
key={idx}
className={classNames('logs__cell--header__item logs__cell logs__text--bold', className)}
role="columnheader"
>
{typeof content === 'string' ? t(content) : content}
</div>;
};
HeaderCell.propTypes = {
content: propTypes.oneOfType([propTypes.string, propTypes.element]).isRequired,
className: propTypes.string,
};
export default HeaderCell;

View File

@@ -1,23 +0,0 @@
import classNames from 'classnames';
import React from 'react';
import { useTranslation } from 'react-i18next';
interface HeaderCellProps {
content: string | React.ReactElement;
className?: string;
}
const HeaderCell = ({ content, className }: HeaderCellProps, idx: any) => {
const { t } = useTranslation();
return (
<div
key={idx}
className={classNames('logs__cell--header__item logs__cell logs__text--bold', className)}
role="columnheader">
{typeof content === 'string' ? t(content) : content}
</div>
);
};
export default HeaderCell;

View File

@@ -91,24 +91,18 @@
margin: -0.5rem 0 0; margin: -0.5rem 0 0;
} }
.grid > .key__time_table_header, .grid > .key__time_table_header, .grid > .key__data, .grid > .key__encryption_status, .grid > .key__elapsed {
.grid > .key__data,
.grid > .key__encryption_status,
.grid > .key__elapsed {
grid-column: 1 / span 1; grid-column: 1 / span 1;
} }
.grid > .value__time_table_header, .grid > .value__time_table_header, .grid > .value__data, .grid > .value__encryption_status, .grid > .value__elapsed {
.grid > .value__data,
.grid > .value__encryption_status,
.grid > .value__elapsed {
grid-column: 2 / span 1; grid-column: 2 / span 1;
margin: 0 !important; margin: 0 !important;
} }
} }
.grid .key-colon:nth-child(odd)::after { .grid .key-colon:nth-child(odd)::after {
content: ':'; content: ":";
} }
.grid__one-row { .grid__one-row {
@@ -133,7 +127,7 @@
} }
.title--border:before { .title--border:before {
content: ''; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
border-top: 0.5px solid var(--gray-d8) !important; border-top: 0.5px solid var(--gray-d8) !important;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
import classNames from 'classnames';
import { processContent } from '../../../helpers/helpers';
import Tooltip from '../../ui/Tooltip';
import 'react-popper-tooltip/dist/styles.css';
import './IconTooltip.css';
import { SHOW_TOOLTIP_DELAY } from '../../../helpers/constants';
const IconTooltip = ({
className,
contentItemClass,
columnClass,
triggerClass,
canShowTooltip = true,
xlinkHref,
title,
placement,
tooltipClass,
content,
trigger,
onVisibilityChange,
defaultTooltipShown,
delayHide,
renderContent = content ? React.Children.map(
processContent(content),
(item, idx) => <div key={idx} className={contentItemClass}>
<Trans>{item || '—'}</Trans>
</div>,
) : null,
}) => {
const tooltipContent = <>
{title
&& <div className="pb-4 h-25 grid-content font-weight-bold"><Trans>{title}</Trans></div>}
<div className={classNames(columnClass)}>{renderContent}</div>
</>;
const tooltipClassName = classNames('tooltip-custom__container', tooltipClass, { 'd-none': !canShowTooltip });
return <Tooltip
className={tooltipClassName}
content={tooltipContent}
placement={placement}
triggerClass={triggerClass}
trigger={trigger}
onVisibilityChange={onVisibilityChange}
delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
delayHide={delayHide}
defaultTooltipShown={defaultTooltipShown}
>
{xlinkHref && <svg className={className}>
<use xlinkHref={`#${xlinkHref}`} />
</svg>}
</Tooltip>;
};
IconTooltip.propTypes = {
className: PropTypes.string,
trigger: PropTypes.string,
triggerClass: PropTypes.string,
contentItemClass: PropTypes.string,
columnClass: PropTypes.string,
tooltipClass: PropTypes.string,
title: PropTypes.string,
placement: PropTypes.string,
canShowTooltip: PropTypes.bool,
xlinkHref: PropTypes.string,
content: PropTypes.node,
renderContent: PropTypes.arrayOf(PropTypes.element),
onVisibilityChange: PropTypes.func,
defaultTooltipShown: PropTypes.bool,
delayHide: PropTypes.number,
};
export default IconTooltip;

View File

@@ -1,94 +0,0 @@
import React from 'react';
import { Trans } from 'react-i18next';
import classNames from 'classnames';
import PopperJS from 'popper.js';
import { TriggerTypes } from 'react-popper-tooltip';
import { processContent } from '../../../helpers/helpers';
import Tooltip from '../../ui/Tooltip';
import 'react-popper-tooltip/dist/styles.css';
import './IconTooltip.css';
import { SHOW_TOOLTIP_DELAY } from '../../../helpers/constants';
interface IconTooltipProps {
className?: string;
trigger?: TriggerTypes;
triggerClass?: string;
contentItemClass?: string;
columnClass?: string;
tooltipClass?: string;
title?: string;
placement?: PopperJS.Placement;
canShowTooltip?: boolean;
xlinkHref?: string;
content?: React.ReactNode;
renderContent?: React.ReactElement[];
onVisibilityChange?: (...args: unknown[]) => unknown;
defaultTooltipShown?: boolean;
delayHide?: number;
}
const IconTooltip = ({
className,
contentItemClass,
columnClass,
triggerClass,
canShowTooltip = true,
xlinkHref,
title,
placement,
tooltipClass,
content,
trigger,
onVisibilityChange,
defaultTooltipShown,
delayHide,
renderContent = content
? React.Children.map(
processContent(content),
(item, idx) => (
<div key={idx} className={contentItemClass}>
<Trans>{item || '—'}</Trans>
</div>
),
)
: null,
}: IconTooltipProps) => {
const tooltipContent = (
<>
{title && (
<div className="pb-4 h-25 grid-content font-weight-bold">
<Trans>{title}</Trans>
</div>
)}
<div className={classNames(columnClass)}>{renderContent}</div>
</>
);
const tooltipClassName = classNames('tooltip-custom__container', tooltipClass, { 'd-none': !canShowTooltip });
return (
<Tooltip
className={tooltipClassName}
content={tooltipContent}
placement={placement}
triggerClass={triggerClass}
trigger={trigger}
onVisibilityChange={onVisibilityChange}
delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
delayHide={delayHide}
defaultTooltipShown={defaultTooltipShown}>
{xlinkHref && (
<svg className={className}>
<use xlinkHref={`#${xlinkHref}`} />
</svg>
)}
</Tooltip>
);
};
export default IconTooltip;

View File

@@ -0,0 +1,141 @@
import { useTranslation } from 'react-i18next';
import { shallowEqual, useSelector } from 'react-redux';
import classNames from 'classnames';
import React from 'react';
import propTypes from 'prop-types';
import {
getRulesToFilterList,
formatElapsedMs,
getFilterNames,
getServiceName,
} from '../../../helpers/helpers';
import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';
import IconTooltip from './IconTooltip';
const ResponseCell = ({
elapsedMs,
originalResponse,
reason,
response,
status,
upstream,
rules,
service_name,
cached,
}) => {
const { t } = useTranslation();
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const services = useSelector((store) => store?.services);
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
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 renderResponses = (responseArr) => {
if (!responseArr || responseArr.length === 0) {
return '';
}
return <div>{responseArr.map((response) => {
const className = classNames('white-space--nowrap', {
'overflow-break': response.length > 100,
});
return <div key={response} className={className}>{`${response}\n`}</div>;
})}</div>;
};
const COMMON_CONTENT = {
encryption_status: boldStatusLabel,
install_settings_dns: upstream,
...(cached
&& {
served_from_cache_label: (
<svg className="icons icon--20 icon--green mb-1">
<use xlinkHref="#check" />
</svg>
),
}
),
elapsed: formattedElapsedMs,
response_code: status,
...(service_name && services.allServices
&& { service_name: getServiceName(services.allServices, service_name) }
),
...(rules.length > 0
&& { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }
),
response_table_header: renderResponses(response),
original_response: renderResponses(originalResponse),
};
const content = rules.length > 0
? Object.entries(COMMON_CONTENT)
: Object.entries({
...COMMON_CONTENT,
filter: '',
});
const getDetailedInfo = (reason) => {
switch (reason) {
case FILTERED_STATUS.FILTERED_BLOCKED_SERVICE:
if (!service_name || !services.allServices) {
return formattedElapsedMs;
}
return getServiceName(services.allServices, service_name);
case FILTERED_STATUS.FILTERED_BLACK_LIST:
case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
return getFilterNames(rules, filters, whitelistFilters).join(', ');
default:
return formattedElapsedMs;
}
};
const detailedInfo = getDetailedInfo(reason);
return (
<div className="logs__cell logs__cell--response" role="gridcell">
<IconTooltip
className={classNames('icons mr-4 icon--24 icon--lightgray logs__question', { 'my-3': isDetailed })}
columnClass='grid grid--limited'
tooltipClass='px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details'
contentItemClass='text-truncate key-colon o-hidden'
xlinkHref='question'
title='response_details'
content={content}
placement='bottom'
/>
<div className="text-truncate">
<div className="text-truncate" title={statusLabel}>{statusLabel}</div>
{isDetailed && <div
className="detailed-info d-none d-sm-block pt-1 text-truncate"
title={detailedInfo}>{detailedInfo}</div>}
</div>
</div>
);
};
ResponseCell.propTypes = {
elapsedMs: propTypes.string.isRequired,
originalResponse: propTypes.array.isRequired,
reason: propTypes.string.isRequired,
response: propTypes.array.isRequired,
status: propTypes.string.isRequired,
upstream: propTypes.string.isRequired,
cached: propTypes.bool.isRequired,
rules: propTypes.arrayOf(propTypes.shape({
text: propTypes.string.isRequired,
filter_list_id: propTypes.number.isRequired,
})),
service_name: propTypes.string,
};
export default ResponseCell;

View File

@@ -1,150 +0,0 @@
import { useTranslation } from 'react-i18next';
import { shallowEqual, useSelector } from 'react-redux';
import classNames from 'classnames';
import React from 'react';
import { getRulesToFilterList, formatElapsedMs, getFilterNames, getServiceName } from '../../../helpers/helpers';
import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';
import IconTooltip from './IconTooltip';
import { RootState } from '../../../initialState';
interface ResponseCellProps {
elapsedMs: string;
originalResponse?: unknown[];
reason: string;
response: unknown[];
status: string;
upstream: string;
cached: boolean;
rules?: {
text: string;
filter_list_id: number;
}[];
service_name?: string;
}
const ResponseCell = ({
elapsedMs,
originalResponse,
reason,
response,
status,
upstream,
rules,
service_name,
cached,
}: ResponseCellProps) => {
const { t } = useTranslation();
const filters = useSelector((state: RootState) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state: RootState) => state.filtering.whitelistFilters, shallowEqual);
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const services = useSelector((store: RootState) => store?.services);
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
const isBlocked =
reason === FILTERED_STATUS.FILTERED_BLACK_LIST || reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
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 renderResponses = (responseArr: any) => {
if (!responseArr || responseArr.length === 0) {
return '';
}
return (
<div>
{responseArr.map((response: any) => {
const className = classNames('white-space--nowrap', {
'overflow-break': response.length > 100,
});
return <div key={response} className={className}>{`${response}\n`}</div>;
})}
</div>
);
};
const COMMON_CONTENT = {
encryption_status: boldStatusLabel,
install_settings_dns: upstream,
...(cached && {
served_from_cache_label: (
<svg className="icons icon--20 icon--green mb-1">
<use xlinkHref="#check" />
</svg>
),
}),
elapsed: formattedElapsedMs,
response_code: status,
...(service_name &&
services.allServices && { service_name: getServiceName(services.allServices, service_name) }),
...(rules.length > 0 && { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }),
response_table_header: renderResponses(response),
original_response: renderResponses(originalResponse),
};
const content =
rules.length > 0
? Object.entries(COMMON_CONTENT)
: Object.entries({
...COMMON_CONTENT,
filter: '',
});
const getDetailedInfo = (reason: any) => {
switch (reason) {
case FILTERED_STATUS.FILTERED_BLOCKED_SERVICE:
if (!service_name || !services.allServices) {
return formattedElapsedMs;
}
return getServiceName(services.allServices, service_name);
case FILTERED_STATUS.FILTERED_BLACK_LIST:
case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
return getFilterNames(rules, filters, whitelistFilters).join(', ');
default:
return formattedElapsedMs;
}
};
const detailedInfo = getDetailedInfo(reason);
return (
<div className="logs__cell logs__cell--response" role="gridcell">
<IconTooltip
className={classNames('icons mr-4 icon--24 icon--lightgray logs__question', { 'my-3': isDetailed })}
columnClass="grid grid--limited"
tooltipClass="px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details"
contentItemClass="text-truncate key-colon o-hidden"
xlinkHref="question"
title="response_details"
content={content}
placement="bottom"
/>
<div className="text-truncate">
<div className="text-truncate" title={statusLabel}>
{statusLabel}
</div>
{isDetailed && (
<div className="detailed-info d-none d-sm-block pt-1 text-truncate" title={detailedInfo}>
{detailedInfo}
</div>
)}
</div>
</div>
);
};
export default ResponseCell;

View File

@@ -2,20 +2,20 @@ import i18next from 'i18next';
export const BUTTON_PREFIX = 'btn_'; export const BUTTON_PREFIX = 'btn_';
export const getBlockClientInfo = (ip: any, disallowed: any, disallowed_rule: any, allowedClients: any) => { export const getBlockClientInfo = (ip, disallowed, disallowed_rule, allowedСlients) => {
let confirmMessage; let confirmMessage;
if (disallowed) { if (disallowed) {
confirmMessage = i18next.t('client_confirm_unblock', { ip: disallowed_rule || ip }); confirmMessage = i18next.t('client_confirm_unblock', { ip: disallowed_rule || ip });
} else { } else {
confirmMessage = `${i18next.t('adg_will_drop_dns_queries')} ${i18next.t('client_confirm_block', { ip })}`; confirmMessage = `${i18next.t('adg_will_drop_dns_queries')} ${i18next.t('client_confirm_block', { ip })}`;
if (allowedClients.length > 0) { if (allowedСlients.length > 0) {
confirmMessage = confirmMessage.concat(`\n\n${i18next.t('filter_allowlist', { disallowed_rule })}`); confirmMessage = confirmMessage.concat(`\n\n${i18next.t('filter_allowlist', { disallowed_rule })}`);
} }
} }
const buttonKey = i18next.t(disallowed ? 'allow_this_client' : 'disallow_this_client'); const buttonKey = i18next.t(disallowed ? 'allow_this_client' : 'disallow_this_client');
const lastRuleInAllowlist = !disallowed && allowedClients === disallowed_rule; const lastRuleInAllowlist = !disallowed && allowedСlients === disallowed_rule;
return { return {
confirmMessage, confirmMessage,

View File

@@ -0,0 +1,285 @@
import React, { memo } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import propTypes from 'prop-types';
import {
captitalizeWords,
checkFiltered,
getRulesToFilterList,
formatDateTime,
formatElapsedMs,
formatTime,
getBlockingClientName,
getServiceName,
processContent,
} from '../../../helpers/helpers';
import {
BLOCK_ACTIONS,
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
FILTERED_STATUS,
FILTERED_STATUS_TO_META_MAP,
LONG_TIME_FORMAT,
QUERY_STATUS_COLORS,
SCHEME_TO_PROTOCOL_MAP,
} from '../../../helpers/constants';
import { getSourceData } from '../../../helpers/trackers/trackers';
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
import DateCell from './DateCell';
import DomainCell from './DomainCell';
import ResponseCell from './ResponseCell';
import ClientCell from './ClientCell';
import { toggleClientBlock } from '../../../actions/access';
import { getBlockClientInfo, BUTTON_PREFIX } from './helpers';
import { updateLogs } from '../../../actions/queryLogs';
import '../Logs.css';
const Row = memo(({
style,
rowProps,
rowProps: { reason },
isSmallScreen,
setDetailedDataCurrent,
setButtonType,
setModalOpened,
}) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
const processingSet = useSelector((state) => state.access.processingSet);
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
const services = useSelector((store) => store?.services);
const clients = useSelector((state) => state.dashboard.clients);
const onClick = () => {
if (!isSmallScreen) {
return;
}
const {
answer_dnssec,
client,
domain,
elapsedMs,
client_info,
response,
time,
tracker,
upstream,
type,
client_proto,
client_id,
rules,
originalResponse,
status,
service_name,
cached,
} = rowProps;
const hasTracker = !!tracker;
const autoClient = autoClients
.find((autoClient) => autoClient.name === client);
const source = autoClient?.source;
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
const isFiltered = checkFiltered(reason);
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const onToggleBlock = () => {
dispatch(toggleBlocking(buttonType, domain));
};
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
const requestStatus = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason);
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
const sourceData = getSourceData(tracker);
const {
confirmMessage,
buttonKey: blockingClientKey,
lastRuleInAllowlist,
} = getBlockClientInfo(
client,
client_info?.disallowed || false,
client_info?.disallowed_rule || '',
allowedСlients,
);
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client);
const onBlockingForClientClick = () => {
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
};
const onBlockingClientClick = async () => {
if (window.confirm(confirmMessage)) {
await dispatch(
toggleClientBlock(
client,
client_info?.disallowed || false,
client_info?.disallowed_rule || '',
),
);
await dispatch(updateLogs());
setModalOpened(false);
}
};
const blockButton = (
<>
<div className="title--border" />
<button
type="button"
className={
classNames(
'button-action--arrow-option mb-1',
{ 'bg--danger': !isBlocked },
{ 'bg--green': isFiltered },
)}
onClick={onToggleBlock}
>
{t(buttonType)}
</button>
</>
);
const blockForClientButton = <button
className='text-center font-weight-bold py-1 button-action--arrow-option'
onClick={onBlockingForClientClick}>
{t(blockingForClientKey)}
</button>;
const blockClientButton = <button
className='text-center font-weight-bold py-1 button-action--arrow-option'
onClick={onBlockingClientClick}
disabled={processingSet || lastRuleInAllowlist}>
{t(blockingClientKey)}
</button>;
const detailedData = {
time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
encryption_status: isBlocked
? <div className="bg--danger">{requestStatus}</div> : requestStatus,
...(FILTERED_STATUS.FILTERED_BLOCKED_SERVICE && service_name && services.allServices
&& { service_name: getServiceName(services.allServices, service_name) }),
domain,
type_table_header: type,
protocol,
known_tracker: hasTracker && 'title',
table_name: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category),
tracker_source: hasTracker && sourceData
&& <a
href={sourceData.url}
target="_blank"
rel="noopener noreferrer"
className="link--green">{sourceData.name}
</a>,
response_details: 'title',
install_settings_dns: upstream,
...(cached
&& {
served_from_cache_label: (
<svg className="icons icon--20 icon--green">
<use xlinkHref="#check" />
</svg>
),
}
),
elapsed: formattedElapsedMs,
...(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: client_info?.name || client_id,
country: client_info?.whois?.country,
city: client_info?.whois?.city,
network: client_info?.whois?.orgname,
source_label: source,
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
original_response: originalResponse?.join('\n'),
[BUTTON_PREFIX + buttonType]: blockButton,
[BUTTON_PREFIX + blockingForClientKey]: blockForClientButton,
[BUTTON_PREFIX + blockingClientKey]: blockClientButton,
};
setDetailedDataCurrent(processContent(detailedData));
setButtonType(buttonType);
setModalOpened(true);
};
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const className = classNames('d-flex px-5 logs__row',
`logs__row--${FILTERED_STATUS_TO_META_MAP?.[reason]?.COLOR ?? QUERY_STATUS_COLORS.WHITE}`, {
'logs__cell--detailed': isDetailed,
});
return <div style={style} className={className} onClick={onClick} role="row">
<DateCell {...rowProps} />
<DomainCell {...rowProps} />
<ResponseCell {...rowProps} />
<ClientCell {...rowProps} />
</div>;
});
Row.displayName = 'Row';
Row.propTypes = {
style: propTypes.object,
rowProps: propTypes.shape({
reason: propTypes.string.isRequired,
answer_dnssec: propTypes.bool.isRequired,
client: propTypes.string.isRequired,
domain: propTypes.string.isRequired,
elapsedMs: propTypes.string.isRequired,
response: propTypes.array.isRequired,
time: propTypes.string.isRequired,
tracker: propTypes.object,
upstream: propTypes.string.isRequired,
cached: propTypes.bool.isRequired,
type: propTypes.string.isRequired,
client_proto: propTypes.string.isRequired,
client_id: propTypes.string,
ecs: propTypes.string,
client_info: propTypes.shape({
name: propTypes.string.isRequired,
whois: propTypes.shape({
country: propTypes.string,
city: propTypes.string,
orgname: propTypes.string,
}).isRequired,
disallowed: propTypes.bool.isRequired,
disallowed_rule: propTypes.string.isRequired,
}),
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,
}).isRequired,
isSmallScreen: propTypes.bool.isRequired,
setDetailedDataCurrent: propTypes.func.isRequired,
setButtonType: propTypes.func.isRequired,
setModalOpened: propTypes.func.isRequired,
};
export default Row;

View File

@@ -1,306 +0,0 @@
import React, { Dispatch, memo, SetStateAction } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import {
captitalizeWords,
checkFiltered,
getRulesToFilterList,
formatDateTime,
formatElapsedMs,
formatTime,
getBlockingClientName,
getServiceName,
processContent,
} from '../../../helpers/helpers';
import {
BLOCK_ACTIONS,
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
FILTERED_STATUS,
FILTERED_STATUS_TO_META_MAP,
LONG_TIME_FORMAT,
QUERY_STATUS_COLORS,
SCHEME_TO_PROTOCOL_MAP,
} from '../../../helpers/constants';
import { getSourceData } from '../../../helpers/trackers/trackers';
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
import DateCell from './DateCell';
import DomainCell from './DomainCell';
import ResponseCell from './ResponseCell';
import ClientCell from './ClientCell';
import { toggleClientBlock } from '../../../actions/access';
import { getBlockClientInfo, BUTTON_PREFIX } from './helpers';
import { updateLogs } from '../../../actions/queryLogs';
import '../Logs.css';
import { RootState } from '../../../initialState';
interface RowProps {
style?: object;
rowProps: {
reason: string;
answer_dnssec: boolean;
client: string;
domain: string;
elapsedMs: string;
response: unknown[];
time: string;
tracker?: {
name: string;
category: string;
};
upstream: string;
cached: boolean;
type: string;
client_proto: string;
client_id?: string;
ecs?: string;
client_info?: {
name: string;
whois: {
country?: string;
city?: string;
orgname?: string;
};
disallowed: boolean;
disallowed_rule: string;
};
rules?: {
text: string;
filter_list_id: number;
}[];
originalResponse?: unknown[];
status: string;
service_name?: string;
};
isSmallScreen: boolean;
setDetailedDataCurrent: Dispatch<SetStateAction<any>>;
setButtonType: (...args: unknown[]) => unknown;
setModalOpened: (...args: unknown[]) => unknown;
}
const Row = memo(
({
style,
rowProps,
rowProps: { reason },
isSmallScreen,
setDetailedDataCurrent,
setButtonType,
setModalOpened,
}: RowProps) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const dnssec_enabled = useSelector((state: RootState) => state.dnsConfig.dnssec_enabled);
const filters = useSelector((state: RootState) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state: RootState) => state.filtering.whitelistFilters, shallowEqual);
const autoClients = useSelector((state: RootState) => state.dashboard.autoClients, shallowEqual);
const processingSet = useSelector((state: RootState) => state.access.processingSet);
const allowedClients = useSelector((state: RootState) => state.access.allowed_clients, shallowEqual);
const services = useSelector((state: RootState) => state?.services);
const clients = useSelector((state: RootState) => state.dashboard.clients);
const onClick = () => {
if (!isSmallScreen) {
return;
}
const {
answer_dnssec,
client,
domain,
elapsedMs,
client_info,
response,
time,
tracker,
upstream,
type,
client_proto,
client_id,
rules,
originalResponse,
status,
service_name,
cached,
} = rowProps;
const hasTracker = !!tracker;
const autoClient = autoClients.find((autoClient: any) => autoClient.name === client);
const source = autoClient?.source;
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
const isFiltered = checkFiltered(reason);
const isBlocked =
reason === FILTERED_STATUS.FILTERED_BLACK_LIST || reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const onToggleBlock = () => {
dispatch(toggleBlocking(buttonType, domain));
};
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
const requestStatus = t(
isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason,
);
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
const sourceData = getSourceData(tracker);
const {
confirmMessage,
buttonKey: blockingClientKey,
lastRuleInAllowlist,
} = getBlockClientInfo(
client,
client_info?.disallowed || false,
client_info?.disallowed_rule || '',
allowedClients,
);
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client);
const onBlockingForClientClick = () => {
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
};
const onBlockingClientClick = async () => {
if (window.confirm(confirmMessage)) {
await dispatch(
toggleClientBlock(client, client_info?.disallowed || false, client_info?.disallowed_rule || ''),
);
await dispatch(updateLogs());
setModalOpened(false);
}
};
const blockButton = (
<>
<div className="title--border" />
<button
type="button"
className={classNames(
'button-action--arrow-option mb-1',
{ 'bg--danger': !isBlocked },
{ 'bg--green': isFiltered },
)}
onClick={onToggleBlock}>
{t(buttonType)}
</button>
</>
);
const blockForClientButton = (
<button
className="text-center font-weight-bold py-1 button-action--arrow-option"
onClick={onBlockingForClientClick}>
{t(blockingForClientKey)}
</button>
);
const blockClientButton = (
<button
className="text-center font-weight-bold py-1 button-action--arrow-option"
onClick={onBlockingClientClick}
disabled={processingSet || lastRuleInAllowlist}>
{t(blockingClientKey)}
</button>
);
const detailedData = {
time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
encryption_status: isBlocked ? <div className="bg--danger">{requestStatus}</div> : requestStatus,
...(FILTERED_STATUS.FILTERED_BLOCKED_SERVICE &&
service_name &&
services.allServices && { service_name: getServiceName(services.allServices, service_name) }),
domain,
type_table_header: type,
protocol,
known_tracker: hasTracker && 'title',
table_name: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category),
tracker_source: hasTracker && sourceData && (
<a href={sourceData.url} target="_blank" rel="noopener noreferrer" className="link--green">
{sourceData.name}
</a>
),
response_details: 'title',
install_settings_dns: upstream,
...(cached && {
served_from_cache_label: (
<svg className="icons icon--20 icon--green">
<use xlinkHref="#check" />
</svg>
),
}),
elapsed: formattedElapsedMs,
...(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: client_info?.name || client_id,
country: client_info?.whois?.country,
city: client_info?.whois?.city,
network: client_info?.whois?.orgname,
source_label: source,
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
original_response: originalResponse?.join('\n'),
[BUTTON_PREFIX + buttonType]: blockButton,
[BUTTON_PREFIX + blockingForClientKey]: blockForClientButton,
[BUTTON_PREFIX + blockingClientKey]: blockClientButton,
};
setDetailedDataCurrent(processContent(detailedData));
setButtonType(buttonType);
setModalOpened(true);
};
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
const className = classNames(
'd-flex px-5 logs__row',
`logs__row--${FILTERED_STATUS_TO_META_MAP?.[reason]?.COLOR ?? QUERY_STATUS_COLORS.WHITE}`,
{
'logs__cell--detailed': isDetailed,
},
);
return (
<div style={style} className={className} onClick={onClick} role="row">
<DateCell {...rowProps} />
<DomainCell {...rowProps} />
<ResponseCell {...rowProps} />
<ClientCell {...rowProps} />
</div>
);
},
);
Row.displayName = 'Row';
export default Row;

View File

@@ -1,6 +1,5 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link'; import { HashLink as Link } from 'react-router-hash-link';
import Card from '../ui/Card'; import Card from '../ui/Card';
@@ -12,7 +11,6 @@ const Disabled = () => (
<Trans>query_log</Trans> <Trans>query_log</Trans>
</h1> </h1>
</div> </div>
<Card> <Card>
<div className="lead text-center py-6"> <div className="lead text-center py-6">
<Trans <Trans
@@ -20,7 +18,8 @@ const Disabled = () => (
<Link to="/settings#logs-config" key="0"> <Link to="/settings#logs-config" key="0">
link link
</Link>, </Link>,
]}> ]}
>
query_log_disabled query_log_disabled
</Trans> </Trans>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More