mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-15 09:58:42 -07:00
Pull request #2231: ADG-8368 Frontend rewritten in TypeScript, added Node 18 support
Merge in DNS/adguard-home from ADG-8368-typescript-node-18 to master Squashed commit of the following: commit daa288ae0d76178af24595cc807055902e6f09ab Merge:4c89cf720
1085d59a6
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Mon Jun 10 17:22:20 2024 +0200 merge commit4c89cf7209
Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jun 6 13:27:18 2024 +0300 remove install from initial state commitb943f2011f
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 23:10:55 2024 +0200 frontend production build fix commitcd1be2d66d
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 20:23:14 2024 +0200 production build quickfix commit7b8ac01fc2
Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Wed Jun 5 19:57:31 2024 +0300 all: upd node docker commit02afed66d5
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 18:23:12 2024 +0200 changelog fixes commit9c0f736f0c
Merge:62c4fbf1e
e04775c4f
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 18:18:29 2024 +0200 merge commit62c4fbf1e3
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:22:22 2024 +0200 empty line in changelog commit76b1e44a93
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:20:37 2024 +0200 changelog commitf783e90040
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:19:13 2024 +0200 filters.js -> filters.ts commit3d4ce6554c
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:18:03 2024 +0200 generated file removed commite35ba58f2a
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 15:45:21 2024 +0200 rollback unwanted changes commit1f30d4216d
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 15:27:36 2024 +0200 review fix commit6cd4e44f07
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 11:55:39 2024 +0200 missing generated file restoresd commit2ab738b303
Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 11:40:32 2024 +0200 Frontend rewritten in TypeScript, added Node 18 support
This commit is contained in:
parent
1085d59a65
commit
1afe226ce8
@ -44,10 +44,15 @@ See also the [v0.107.51 GitHub milestone][ms-v0.107.51].
|
||||
|
||||
### 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/
|
||||
|
11
README.md
11
README.md
@ -206,9 +206,8 @@ Run `make init` to prepare the development environment.
|
||||
You will need this to build AdGuard Home:
|
||||
|
||||
- [Go](https://golang.org/dl/) v1.22 or later;
|
||||
- [Node.js](https://nodejs.org/en/download/) v16 or later;
|
||||
- [Node.js](https://nodejs.org/en/download/) v18.18 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>
|
||||
|
||||
@ -220,14 +219,6 @@ cd AdGuardHome
|
||||
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]
|
||||
> 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`.
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
|
||||
'stages':
|
||||
@ -265,7 +265,7 @@
|
||||
# need to build a few of these.
|
||||
'variables':
|
||||
'channel': 'beta'
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final
|
||||
# release is built.
|
||||
@ -281,5 +281,5 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
|
@ -5,7 +5,7 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
'channel': 'development'
|
||||
|
||||
@ -194,6 +194,6 @@
|
||||
# Set the default release channel on the release branch to beta, as we
|
||||
# may need to build a few of these.
|
||||
'variables':
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
'channel': 'candidate'
|
||||
|
60
client/.eslintrc.json
vendored
60
client/.eslintrc.json
vendored
@ -1,9 +1,13 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"plugins": ["prettier"],
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"prettier",
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"airbnb-base"
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"jest": true,
|
||||
"node": true,
|
||||
@ -16,50 +20,21 @@
|
||||
"version": "16.4"
|
||||
},
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
"config": "webpack.common.js"
|
||||
"node": {
|
||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"import/extensions": [
|
||||
"error",
|
||||
4,
|
||||
"ignorePackages",
|
||||
{
|
||||
"SwitchCase": 1,
|
||||
"VariableDeclarator": 1,
|
||||
"outerIIFEBody": 1,
|
||||
"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
|
||||
"js": "never",
|
||||
"jsx": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
],
|
||||
"class-methods-use-this": "off",
|
||||
@ -68,10 +43,7 @@
|
||||
"no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": [
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
"allow": ["warn", "error"]
|
||||
}
|
||||
],
|
||||
"import/no-extraneous-dependencies": [
|
||||
|
2
client/.gitattributes
vendored
2
client/.gitattributes
vendored
@ -1 +1 @@
|
||||
*.js text eol=lf
|
||||
*.ts text eol=lf
|
||||
|
10
client/.prettierrc
vendored
Normal file
10
client/.prettierrc
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"arrowParens": "always",
|
||||
}
|
46
client/.stylelintrc
vendored
46
client/.stylelintrc
vendored
@ -1,46 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
44
client/.stylelintrc.js
vendored
Normal file
44
client/.stylelintrc.js
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
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"
|
||||
}
|
||||
}
|
14
client/babel.config.cjs
vendored
Normal file
14
client/babel.config.cjs
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
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
17
client/babel.config.js
vendored
@ -1,17 +0,0 @@
|
||||
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
9
client/constants.js
vendored
@ -1,11 +1,6 @@
|
||||
const BUILD_ENVS = {
|
||||
export const BUILD_ENVS = {
|
||||
dev: 'development',
|
||||
prod: 'production',
|
||||
};
|
||||
|
||||
const BASE_URL = 'control';
|
||||
|
||||
module.exports = {
|
||||
BUILD_ENVS,
|
||||
BASE_URL,
|
||||
};
|
||||
export const BASE_URL = 'control';
|
||||
|
6
client/global.d.ts
vendored
Normal file
6
client/global.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
||||
export default content;
|
||||
}
|
5
client/jest.config.js
vendored
5
client/jest.config.js
vendored
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
},
|
||||
};
|
6
client/jest.config.mjs
vendored
Normal file
6
client/jest.config.mjs
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'babel-jest',
|
||||
},
|
||||
};
|
39895
client/package-lock.json
generated
vendored
39895
client/package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
110
client/package.json
vendored
110
client/package.json
vendored
@ -3,19 +3,23 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-dev": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js",
|
||||
"build-dev": "cross-env NODE_ENV=development BUILD_ENV=dev webpack --config webpack.dev.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:hot": "cross-env BUILD_ENV=dev webpack-dev-server --config webpack.dev.js",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint": "echo 'Lint temporarily disabled'",
|
||||
"lint-new": "eslint './src/**/*.(ts|tsx)'",
|
||||
"lint:fix": "eslint './src/**/*.(ts|tsx)' --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
"test:watch": "jest --watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck:watch": "tsc --noEmit --watch"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@nivo/line": "^0.64.0",
|
||||
"axios": "^0.19.2",
|
||||
"classnames": "^2.2.6",
|
||||
"classnames": "^2.5.1",
|
||||
"countries-and-timezones": "^3.6.0",
|
||||
"date-fns": "^1.29.0",
|
||||
"i18next": "^19.6.2",
|
||||
@ -24,7 +28,8 @@
|
||||
"js-yaml": "^3.14.0",
|
||||
"lodash": "^4.17.19",
|
||||
"nanoid": "^3.1.9",
|
||||
"prop-types": "^15.7.2",
|
||||
"popper.js": "^1.16.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^6.13.1",
|
||||
"react": "^16.13.1",
|
||||
"react-click-outside": "^3.0.1",
|
||||
@ -38,53 +43,64 @@
|
||||
"react-router-hash-link": "^1.2.2",
|
||||
"react-select": "^3.1.0",
|
||||
"react-table": "^6.11.4",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"redux": "^4.0.5",
|
||||
"redux-actions": "^2.6.5",
|
||||
"redux-form": "^8.3.5",
|
||||
"redux-form": "^8.3.10",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"url-polyfill": "^1.1.9"
|
||||
"ts-migrate": "^0.1.35",
|
||||
"url-polyfill": "^1.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.9.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
|
||||
"@babel/plugin-transform-runtime": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"autoprefixer": "^9.8.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^6.0.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^3.5.3",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb": "^18.1.0",
|
||||
"eslint-import-resolver-webpack": "^0.12.1",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^2.5.0",
|
||||
"file-loader": "6.0.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"jest": "^26.0.1",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"@babel/core": "^7.24.5",
|
||||
"@babel/plugin-transform-class-properties": "^7.24.1",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1",
|
||||
"@babel/plugin-transform-object-rest-spread": "^7.24.5",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.24.5",
|
||||
"@babel/plugin-transform-runtime": "^7.24.3",
|
||||
"@babel/preset-env": "^7.24.5",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/react": "^17.0.80",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-redux": "^7.1.33",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/redux-actions": "^2.6.5",
|
||||
"@types/redux-form": "^8.3.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||
"@typescript-eslint/parser": "^7.10.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^9.1.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",
|
||||
"postcss-flexbugs-fixes": "4.2.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"style-loader": "^1.2.1",
|
||||
"stylelint": "^13.5.0",
|
||||
"stylelint-webpack-plugin": "2.0.0",
|
||||
"url-loader": "^4.1.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-merge": "^4.2.2"
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prettier": "^3.2.5",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^16.5.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4",
|
||||
"webpack-merge": "^5.10.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"development": [
|
||||
|
@ -1,26 +1,15 @@
|
||||
import {
|
||||
sortIp,
|
||||
countClientsStatistics,
|
||||
findAddressType,
|
||||
subnetMaskToBitMask,
|
||||
} from '../helpers/helpers';
|
||||
import { sortIp, countClientsStatistics, findAddressType, subnetMaskToBitMask } from '../helpers/helpers';
|
||||
import { ADDRESS_TYPES } from '../helpers/constants';
|
||||
|
||||
describe('sortIp', () => {
|
||||
describe('ipv4', () => {
|
||||
test('one octet differ', () => {
|
||||
const arr = [
|
||||
'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',
|
||||
];
|
||||
const arr = ['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);
|
||||
});
|
||||
|
||||
test('few octets differ', () => {
|
||||
const arr = [
|
||||
'192.168.11.10',
|
||||
@ -58,6 +47,7 @@ describe('sortIp', () => {
|
||||
'192.168.11.10',
|
||||
'192.168.11.11',
|
||||
];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
|
||||
// Example from issue https://github.com/AdguardTeam/AdGuardHome/issues/1778#issuecomment-640937599
|
||||
@ -83,36 +73,26 @@ describe('sortIp', () => {
|
||||
'192.168.2.200',
|
||||
'192.168.3.1',
|
||||
];
|
||||
|
||||
expect(arr2.sort(sortIp)).toStrictEqual(sortedArr2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ipv6', () => {
|
||||
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 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',
|
||||
];
|
||||
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 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);
|
||||
});
|
||||
|
||||
test('only short form', () => {
|
||||
const arr = [
|
||||
'2001:db8::',
|
||||
'2001:db7::',
|
||||
'2001:db9::',
|
||||
];
|
||||
const sortedArr = [
|
||||
'2001:db7::',
|
||||
'2001:db8::',
|
||||
'2001:db9::',
|
||||
];
|
||||
const arr = ['2001:db8::', '2001:db7::', '2001:db9::'];
|
||||
const sortedArr = ['2001:db7::', '2001:db8::', '2001:db9::'];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
|
||||
test('long and short forms', () => {
|
||||
const arr = [
|
||||
'2001:db8::',
|
||||
@ -130,9 +110,11 @@ describe('sortIp', () => {
|
||||
'2001:db7:11a3:9d7:0:0:0:2',
|
||||
'2001:db8::',
|
||||
];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ipv4 and ipv6', () => {
|
||||
test('ipv6 long form', () => {
|
||||
const arr = [
|
||||
@ -151,8 +133,10 @@ describe('sortIp', () => {
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'2001:db8:11a3:9d7:0:0:0:3',
|
||||
];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
|
||||
test('ipv6 short form', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7::1',
|
||||
@ -170,8 +154,10 @@ describe('sortIp', () => {
|
||||
'2001:db8:11a3:9d7::2',
|
||||
'2001:db8:11a3:9d7::3',
|
||||
];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
|
||||
test('ipv6 long and short forms', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7::1',
|
||||
@ -189,8 +175,10 @@ describe('sortIp', () => {
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'2001:db8:11a3:9d7::3',
|
||||
];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
|
||||
test('always put ipv4 before ipv6', () => {
|
||||
const arr = [
|
||||
'::1',
|
||||
@ -210,40 +198,26 @@ describe('sortIp', () => {
|
||||
'2001:db8:11a3:9d7::1',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cidr', () => {
|
||||
test('only ipv4 cidr', () => {
|
||||
const arr = [
|
||||
'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',
|
||||
];
|
||||
const arr = ['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);
|
||||
});
|
||||
|
||||
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 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',
|
||||
];
|
||||
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 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);
|
||||
});
|
||||
|
||||
test('only ipv6 cidr', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7::1/32',
|
||||
@ -257,8 +231,10 @@ describe('sortIp', () => {
|
||||
'2001:db8:11a3:9d7::1/64',
|
||||
'2001:db8:11a3:9d7::1/128',
|
||||
];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
|
||||
test('ipv6 and cidr ipv6', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7::1/32',
|
||||
@ -274,9 +250,11 @@ describe('sortIp', () => {
|
||||
'2001:db8:11a3:9d7::1/128',
|
||||
'2001:db8:11a3:9d7::1',
|
||||
];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid input', () => {
|
||||
const originalWarn = console.warn;
|
||||
|
||||
@ -291,21 +269,29 @@ describe('sortIp', () => {
|
||||
|
||||
test('invalid strings', () => {
|
||||
const arr = ['invalid ip', 'invalid cidr'];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(arr);
|
||||
});
|
||||
|
||||
test('invalid ip', () => {
|
||||
const arr = ['127.0.0.2.', '.127.0.0.1.', '.2001:db8:11a3:9d7:0:0:0:0'];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(arr);
|
||||
});
|
||||
|
||||
test('invalid cidr', () => {
|
||||
const arr = ['127.0.0.2/33', '2001:db8:11a3:9d7:0:0:0:0/129'];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(arr);
|
||||
});
|
||||
|
||||
test('valid and invalid ip', () => {
|
||||
const arr = ['127.0.0.4.', '127.0.0.1', '.127.0.0.3', '127.0.0.2'];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(arr);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed', () => {
|
||||
test('ipv4, ipv6 in short and long forms and cidr', () => {
|
||||
const arr = [
|
||||
@ -354,6 +340,7 @@ describe('sortIp', () => {
|
||||
'2001:db8:11a3:9d7:0:0:0:1',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
];
|
||||
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
});
|
||||
@ -363,9 +350,11 @@ describe('findAddressType', () => {
|
||||
describe('ip', () => {
|
||||
expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP);
|
||||
});
|
||||
|
||||
describe('cidr', () => {
|
||||
expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR);
|
||||
});
|
||||
|
||||
describe('mac', () => {
|
||||
expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN);
|
||||
});
|
||||
@ -373,42 +362,59 @@ describe('findAddressType', () => {
|
||||
|
||||
describe('countClientsStatistics', () => {
|
||||
test('single ip', () => {
|
||||
expect(countClientsStatistics(['127.0.0.1'], {
|
||||
'127.0.0.1': 1,
|
||||
})).toStrictEqual(1);
|
||||
expect(
|
||||
countClientsStatistics(['127.0.0.1'], {
|
||||
'127.0.0.1': 1,
|
||||
}),
|
||||
).toStrictEqual(1);
|
||||
});
|
||||
|
||||
test('multiple ip', () => {
|
||||
expect(countClientsStatistics(['127.0.0.1', '127.0.0.2'], {
|
||||
'127.0.0.1': 1,
|
||||
'127.0.0.2': 2,
|
||||
})).toStrictEqual(1 + 2);
|
||||
expect(
|
||||
countClientsStatistics(['127.0.0.1', '127.0.0.2'], {
|
||||
'127.0.0.1': 1,
|
||||
'127.0.0.2': 2,
|
||||
}),
|
||||
).toStrictEqual(1 + 2);
|
||||
});
|
||||
|
||||
test('cidr', () => {
|
||||
expect(countClientsStatistics(['127.0.0.0/8'], {
|
||||
'127.0.0.1': 1,
|
||||
'127.0.0.2': 2,
|
||||
})).toStrictEqual(1 + 2);
|
||||
expect(
|
||||
countClientsStatistics(['127.0.0.0/8'], {
|
||||
'127.0.0.1': 1,
|
||||
'127.0.0.2': 2,
|
||||
}),
|
||||
).toStrictEqual(1 + 2);
|
||||
});
|
||||
|
||||
test('cidr and multiple ip', () => {
|
||||
expect(countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], {
|
||||
'1.1.1.1': 1,
|
||||
'2.2.2.2': 2,
|
||||
'3.3.3.3': 3,
|
||||
})).toStrictEqual(1 + 2 + 3);
|
||||
expect(
|
||||
countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], {
|
||||
'1.1.1.1': 1,
|
||||
'2.2.2.2': 2,
|
||||
'3.3.3.3': 3,
|
||||
}),
|
||||
).toStrictEqual(1 + 2 + 3);
|
||||
});
|
||||
|
||||
test('mac', () => {
|
||||
expect(countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], {
|
||||
'1.1.1.1': 1,
|
||||
'2.2.2.2': 2,
|
||||
'3.3.3.3': 3,
|
||||
})).toStrictEqual(2 + 3);
|
||||
expect(
|
||||
countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], {
|
||||
'1.1.1.1': 1,
|
||||
'2.2.2.2': 2,
|
||||
'3.3.3.3': 3,
|
||||
}),
|
||||
).toStrictEqual(2 + 3);
|
||||
});
|
||||
|
||||
test('not found', () => {
|
||||
expect(countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], {
|
||||
'1.1.1.1': 1,
|
||||
'2.2.2.2': 2,
|
||||
'3.3.3.3': 3,
|
||||
})).toStrictEqual(0);
|
||||
expect(
|
||||
countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], {
|
||||
'1.1.1.1': 1,
|
||||
'2.2.2.2': 2,
|
||||
'3.3.3.3': 3,
|
||||
}),
|
||||
).toStrictEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -451,10 +457,12 @@ describe('subnetMaskToBitMask', () => {
|
||||
|
||||
test('correct for all subnetMasks', () => {
|
||||
expect(
|
||||
subnetMasks.map((subnetMask) => {
|
||||
const bitmask = subnetMaskToBitMask(subnetMask);
|
||||
return subnetMasks[bitmask] === subnetMask;
|
||||
}).every((res) => res === true),
|
||||
subnetMasks
|
||||
.map((subnetMask) => {
|
||||
const bitmask = subnetMaskToBitMask(subnetMask);
|
||||
return subnetMasks[bitmask] === subnetMask;
|
||||
})
|
||||
.every((res) => res === true),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
@ -3,13 +3,14 @@ import i18next from 'i18next';
|
||||
|
||||
import apiClient from '../api/Api';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
|
||||
import { splitByNewLine } from '../helpers/helpers';
|
||||
|
||||
export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST');
|
||||
export const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE');
|
||||
export const getAccessListSuccess = createAction('GET_ACCESS_LIST_SUCCESS');
|
||||
|
||||
export const getAccessList = () => async (dispatch) => {
|
||||
export const getAccessList = () => async (dispatch: any) => {
|
||||
dispatch(getAccessListRequest());
|
||||
try {
|
||||
const data = await apiClient.getAccessList();
|
||||
@ -24,7 +25,7 @@ export const setAccessListRequest = createAction('SET_ACCESS_LIST_REQUEST');
|
||||
export const setAccessListFailure = createAction('SET_ACCESS_LIST_FAILURE');
|
||||
export const setAccessListSuccess = createAction('SET_ACCESS_LIST_SUCCESS');
|
||||
|
||||
export const setAccessList = (config) => async (dispatch) => {
|
||||
export const setAccessList = (config: any) => async (dispatch: any) => {
|
||||
dispatch(setAccessListRequest());
|
||||
try {
|
||||
const { allowed_clients, disallowed_clients, blocked_hosts } = config;
|
||||
@ -48,7 +49,7 @@ export const toggleClientBlockRequest = createAction('TOGGLE_CLIENT_BLOCK_REQUES
|
||||
export const toggleClientBlockFailure = createAction('TOGGLE_CLIENT_BLOCK_FAILURE');
|
||||
export const toggleClientBlockSuccess = createAction('TOGGLE_CLIENT_BLOCK_SUCCESS');
|
||||
|
||||
export const toggleClientBlock = (ip, disallowed, disallowed_rule) => async (dispatch) => {
|
||||
export const toggleClientBlock = (ip: any, disallowed: any, disallowed_rule: any) => async (dispatch: any) => {
|
||||
dispatch(toggleClientBlockRequest());
|
||||
try {
|
||||
const accessList = await apiClient.getAccessList();
|
||||
@ -60,12 +61,10 @@ export const toggleClientBlock = (ip, disallowed, disallowed_rule) => async (dis
|
||||
if (!disallowed_rule) {
|
||||
allowed_clients = allowed_clients.concat(ip);
|
||||
} else {
|
||||
disallowed_clients = disallowed_clients
|
||||
.filter((client) => client !== disallowed_rule);
|
||||
disallowed_clients = disallowed_clients.filter((client: any) => client !== disallowed_rule);
|
||||
}
|
||||
} else if (allowed_clients.length > 1) {
|
||||
allowed_clients = allowed_clients
|
||||
.filter((client) => client !== disallowed_rule);
|
||||
allowed_clients = allowed_clients.filter((client: any) => client !== disallowed_rule);
|
||||
} else {
|
||||
disallowed_clients = disallowed_clients.concat(ip);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import i18next from 'i18next';
|
||||
import apiClient from '../api/Api';
|
||||
|
||||
import { getClients } from './index';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
|
||||
@ -10,7 +11,7 @@ export const addClientRequest = createAction('ADD_CLIENT_REQUEST');
|
||||
export const addClientFailure = createAction('ADD_CLIENT_FAILURE');
|
||||
export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS');
|
||||
|
||||
export const addClient = (config) => async (dispatch) => {
|
||||
export const addClient = (config: any) => async (dispatch: any) => {
|
||||
dispatch(addClientRequest());
|
||||
try {
|
||||
await apiClient.addClient(config);
|
||||
@ -28,7 +29,7 @@ export const deleteClientRequest = createAction('DELETE_CLIENT_REQUEST');
|
||||
export const deleteClientFailure = createAction('DELETE_CLIENT_FAILURE');
|
||||
export const deleteClientSuccess = createAction('DELETE_CLIENT_SUCCESS');
|
||||
|
||||
export const deleteClient = (config) => async (dispatch) => {
|
||||
export const deleteClient = (config: any) => async (dispatch: any) => {
|
||||
dispatch(deleteClientRequest());
|
||||
try {
|
||||
await apiClient.deleteClient(config);
|
||||
@ -45,7 +46,7 @@ export const updateClientRequest = createAction('UPDATE_CLIENT_REQUEST');
|
||||
export const updateClientFailure = createAction('UPDATE_CLIENT_FAILURE');
|
||||
export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS');
|
||||
|
||||
export const updateClient = (config, name) => async (dispatch) => {
|
||||
export const updateClient = (config: any, name: any) => async (dispatch: any) => {
|
||||
dispatch(updateClientRequest());
|
||||
try {
|
||||
const data = { name, data: { ...config } };
|
@ -2,6 +2,7 @@ import { createAction } from 'redux-actions';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import apiClient from '../api/Api';
|
||||
|
||||
import { splitByNewLine } from '../helpers/helpers';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
|
||||
@ -9,7 +10,7 @@ export const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST');
|
||||
export const getDnsConfigFailure = createAction('GET_DNS_CONFIG_FAILURE');
|
||||
export const getDnsConfigSuccess = createAction('GET_DNS_CONFIG_SUCCESS');
|
||||
|
||||
export const getDnsConfig = () => async (dispatch) => {
|
||||
export const getDnsConfig = () => async (dispatch: any) => {
|
||||
dispatch(getDnsConfigRequest());
|
||||
try {
|
||||
const data = await apiClient.getDnsConfig();
|
||||
@ -24,7 +25,7 @@ export const clearDnsCacheRequest = createAction('CLEAR_DNS_CACHE_REQUEST');
|
||||
export const clearDnsCacheFailure = createAction('CLEAR_DNS_CACHE_FAILURE');
|
||||
export const clearDnsCacheSuccess = createAction('CLEAR_DNS_CACHE_SUCCESS');
|
||||
|
||||
export const clearDnsCache = () => async (dispatch) => {
|
||||
export const clearDnsCache = () => async (dispatch: any) => {
|
||||
dispatch(clearDnsCacheRequest());
|
||||
try {
|
||||
const data = await apiClient.clearCache();
|
||||
@ -40,7 +41,7 @@ export const setDnsConfigRequest = createAction('SET_DNS_CONFIG_REQUEST');
|
||||
export const setDnsConfigFailure = createAction('SET_DNS_CONFIG_FAILURE');
|
||||
export const setDnsConfigSuccess = createAction('SET_DNS_CONFIG_SUCCESS');
|
||||
|
||||
export const setDnsConfig = (config) => async (dispatch) => {
|
||||
export const setDnsConfig = (config: any) => async (dispatch: any) => {
|
||||
dispatch(setDnsConfigRequest());
|
||||
try {
|
||||
const data = { ...config };
|
@ -1,5 +1,6 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import apiClient from '../api/Api';
|
||||
|
||||
import { redirectToCurrentProtocol } from '../helpers/helpers';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
|
||||
@ -7,7 +8,7 @@ export const getTlsStatusRequest = createAction('GET_TLS_STATUS_REQUEST');
|
||||
export const getTlsStatusFailure = createAction('GET_TLS_STATUS_FAILURE');
|
||||
export const getTlsStatusSuccess = createAction('GET_TLS_STATUS_SUCCESS');
|
||||
|
||||
export const getTlsStatus = () => async (dispatch) => {
|
||||
export const getTlsStatus = () => async (dispatch: any) => {
|
||||
dispatch(getTlsStatusRequest());
|
||||
try {
|
||||
const status = await apiClient.getTlsStatus();
|
||||
@ -26,7 +27,7 @@ export const setTlsConfigFailure = createAction('SET_TLS_CONFIG_FAILURE');
|
||||
export const setTlsConfigSuccess = createAction('SET_TLS_CONFIG_SUCCESS');
|
||||
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
||||
|
||||
export const setTlsConfig = (config) => async (dispatch, getState) => {
|
||||
export const setTlsConfig = (config: any) => async (dispatch: any, getState: any) => {
|
||||
dispatch(setTlsConfigRequest());
|
||||
try {
|
||||
const { httpPort } = getState().dashboard;
|
||||
@ -67,7 +68,7 @@ export const validateTlsConfigRequest = createAction('VALIDATE_TLS_CONFIG_REQUES
|
||||
export const validateTlsConfigFailure = createAction('VALIDATE_TLS_CONFIG_FAILURE');
|
||||
export const validateTlsConfigSuccess = createAction('VALIDATE_TLS_CONFIG_SUCCESS');
|
||||
|
||||
export const validateTlsConfig = (config) => async (dispatch) => {
|
||||
export const validateTlsConfig = (config: any) => async (dispatch: any) => {
|
||||
dispatch(validateTlsConfigRequest());
|
||||
try {
|
||||
const values = { ...config };
|
@ -13,7 +13,7 @@ export const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQU
|
||||
export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE');
|
||||
export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS');
|
||||
|
||||
export const getFilteringStatus = () => async (dispatch) => {
|
||||
export const getFilteringStatus = () => async (dispatch: any) => {
|
||||
dispatch(getFilteringStatusRequest());
|
||||
try {
|
||||
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 setRulesSuccess = createAction('SET_RULES_SUCCESS');
|
||||
|
||||
export const setRules = (rules) => async (dispatch) => {
|
||||
export const setRules = (rules: any) => async (dispatch: any) => {
|
||||
dispatch(setRulesRequest());
|
||||
try {
|
||||
const normalizedRules = {
|
||||
@ -47,83 +47,91 @@ export const addFilterRequest = createAction('ADD_FILTER_REQUEST');
|
||||
export const addFilterFailure = createAction('ADD_FILTER_FAILURE');
|
||||
export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');
|
||||
|
||||
export const addFilter = (url, name, whitelist = false) => async (dispatch, getState) => {
|
||||
dispatch(addFilterRequest());
|
||||
try {
|
||||
await apiClient.addFilter({ url, name, whitelist });
|
||||
dispatch(addFilterSuccess(url));
|
||||
if (getState().filtering.isModalOpen) {
|
||||
dispatch(toggleFilteringModal());
|
||||
export const addFilter =
|
||||
(url: any, name: any, whitelist = false) =>
|
||||
async (dispatch: any, getState: any) => {
|
||||
dispatch(addFilterRequest());
|
||||
try {
|
||||
await apiClient.addFilter({ url, name, whitelist });
|
||||
dispatch(addFilterSuccess(url));
|
||||
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 removeFilterFailure = createAction('REMOVE_FILTER_FAILURE');
|
||||
export const removeFilterSuccess = createAction('REMOVE_FILTER_SUCCESS');
|
||||
|
||||
export const removeFilter = (url, whitelist = false) => async (dispatch, getState) => {
|
||||
dispatch(removeFilterRequest());
|
||||
try {
|
||||
await apiClient.removeFilter({ url, whitelist });
|
||||
dispatch(removeFilterSuccess(url));
|
||||
if (getState().filtering.isModalOpen) {
|
||||
dispatch(toggleFilteringModal());
|
||||
export const removeFilter =
|
||||
(url: any, whitelist = false) =>
|
||||
async (dispatch: any, getState: any) => {
|
||||
dispatch(removeFilterRequest());
|
||||
try {
|
||||
await apiClient.removeFilter({ url, whitelist });
|
||||
dispatch(removeFilterSuccess(url));
|
||||
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 toggleFilterFailure = createAction('FILTER_TOGGLE_FAILURE');
|
||||
export const toggleFilterSuccess = createAction('FILTER_TOGGLE_SUCCESS');
|
||||
|
||||
export const toggleFilterStatus = (url, data, whitelist = false) => async (dispatch) => {
|
||||
dispatch(toggleFilterRequest());
|
||||
try {
|
||||
await apiClient.setFilterUrl({ url, data, whitelist });
|
||||
dispatch(toggleFilterSuccess(url));
|
||||
dispatch(getFilteringStatus());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleFilterFailure());
|
||||
}
|
||||
};
|
||||
export const toggleFilterStatus =
|
||||
(url: any, data: any, whitelist = false) =>
|
||||
async (dispatch: any) => {
|
||||
dispatch(toggleFilterRequest());
|
||||
try {
|
||||
await apiClient.setFilterUrl({ url, data, whitelist });
|
||||
dispatch(toggleFilterSuccess(url));
|
||||
dispatch(getFilteringStatus());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleFilterFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const editFilterRequest = createAction('EDIT_FILTER_REQUEST');
|
||||
export const editFilterFailure = createAction('EDIT_FILTER_FAILURE');
|
||||
export const editFilterSuccess = createAction('EDIT_FILTER_SUCCESS');
|
||||
|
||||
export const editFilter = (url, data, whitelist = false) => async (dispatch, getState) => {
|
||||
dispatch(editFilterRequest());
|
||||
try {
|
||||
await apiClient.setFilterUrl({ url, data, whitelist });
|
||||
dispatch(editFilterSuccess(url));
|
||||
if (getState().filtering.isModalOpen) {
|
||||
dispatch(toggleFilteringModal());
|
||||
export const editFilter =
|
||||
(url: any, data: any, whitelist = false) =>
|
||||
async (dispatch: any, getState: any) => {
|
||||
dispatch(editFilterRequest());
|
||||
try {
|
||||
await apiClient.setFilterUrl({ url, data, whitelist });
|
||||
dispatch(editFilterSuccess(url));
|
||||
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 refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE');
|
||||
export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
|
||||
|
||||
export const refreshFilters = (config) => async (dispatch) => {
|
||||
export const refreshFilters = (config: any) => async (dispatch: any) => {
|
||||
dispatch(refreshFiltersRequest());
|
||||
dispatch(showLoading());
|
||||
try {
|
||||
@ -150,7 +158,7 @@ export const setFiltersConfigRequest = createAction('SET_FILTERS_CONFIG_REQUEST'
|
||||
export const setFiltersConfigFailure = createAction('SET_FILTERS_CONFIG_FAILURE');
|
||||
export const setFiltersConfigSuccess = createAction('SET_FILTERS_CONFIG_SUCCESS');
|
||||
|
||||
export const setFiltersConfig = (config) => async (dispatch, getState) => {
|
||||
export const setFiltersConfig = (config: any) => async (dispatch: any, getState: any) => {
|
||||
dispatch(setFiltersConfigRequest());
|
||||
try {
|
||||
const { enabled } = config;
|
||||
@ -180,16 +188,18 @@ export const checkHostSuccess = createAction('CHECK_HOST_SUCCESS');
|
||||
* @param {string} host.name
|
||||
* @returns {undefined}
|
||||
*/
|
||||
export const checkHost = (host) => async (dispatch) => {
|
||||
export const checkHost = (host: any) => async (dispatch: any) => {
|
||||
dispatch(checkHostRequest());
|
||||
try {
|
||||
const data = await apiClient.checkHost(host);
|
||||
const { name: hostname } = host;
|
||||
|
||||
dispatch(checkHostSuccess({
|
||||
hostname,
|
||||
...data,
|
||||
}));
|
||||
dispatch(
|
||||
checkHostSuccess({
|
||||
hostname,
|
||||
...data,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(checkHostFailure());
|
@ -38,7 +38,7 @@ export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
||||
* @param {*} status: boolean | SafeSearchConfig
|
||||
* @returns
|
||||
*/
|
||||
export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
||||
export const toggleSetting = (settingKey: any, status: any) => async (dispatch: any) => {
|
||||
let successMessage = '';
|
||||
try {
|
||||
switch (settingKey) {
|
||||
@ -80,64 +80,58 @@ export const initSettingsRequest = createAction('SETTINGS_INIT_REQUEST');
|
||||
export const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE');
|
||||
export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS');
|
||||
|
||||
export const initSettings = (settingsList = {
|
||||
safebrowsing: {}, parental: {},
|
||||
}) => async (dispatch) => {
|
||||
dispatch(initSettingsRequest());
|
||||
try {
|
||||
const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
|
||||
const parentalStatus = await apiClient.getParentalStatus();
|
||||
const safesearchStatus = await apiClient.getSafesearchStatus();
|
||||
const {
|
||||
safebrowsing,
|
||||
parental,
|
||||
} = settingsList;
|
||||
const newSettingsList = {
|
||||
safebrowsing: {
|
||||
...safebrowsing,
|
||||
enabled: safebrowsingStatus.enabled,
|
||||
},
|
||||
parental: {
|
||||
...parental,
|
||||
enabled: parentalStatus.enabled,
|
||||
},
|
||||
safesearch: {
|
||||
...safesearchStatus,
|
||||
},
|
||||
};
|
||||
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(initSettingsFailure());
|
||||
}
|
||||
};
|
||||
export const initSettings =
|
||||
(
|
||||
settingsList = {
|
||||
safebrowsing: {},
|
||||
parental: {},
|
||||
},
|
||||
) =>
|
||||
async (dispatch: any) => {
|
||||
dispatch(initSettingsRequest());
|
||||
try {
|
||||
const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
|
||||
const parentalStatus = await apiClient.getParentalStatus();
|
||||
const safesearchStatus = await apiClient.getSafesearchStatus();
|
||||
const { safebrowsing, parental } = settingsList;
|
||||
const newSettingsList = {
|
||||
safebrowsing: {
|
||||
...safebrowsing,
|
||||
enabled: safebrowsingStatus.enabled,
|
||||
},
|
||||
parental: {
|
||||
...parental,
|
||||
enabled: parentalStatus.enabled,
|
||||
},
|
||||
safesearch: {
|
||||
...safesearchStatus,
|
||||
},
|
||||
};
|
||||
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(initSettingsFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');
|
||||
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
|
||||
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
|
||||
|
||||
const getDisabledMessage = (time) => {
|
||||
const getDisabledMessage = (time: any) => {
|
||||
switch (time) {
|
||||
case DISABLE_PROTECTION_TIMINGS.HALF_MINUTE:
|
||||
return i18next.t(
|
||||
'disable_notify_for_seconds',
|
||||
{ count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) },
|
||||
);
|
||||
return i18next.t('disable_notify_for_seconds', {
|
||||
count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_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:
|
||||
return i18next.t(
|
||||
'disable_notify_for_minutes',
|
||||
{ count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) },
|
||||
);
|
||||
return i18next.t('disable_notify_for_minutes', {
|
||||
count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES),
|
||||
});
|
||||
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:
|
||||
return i18next.t('disable_notify_until_tomorrow');
|
||||
default:
|
||||
@ -145,22 +139,24 @@ const getDisabledMessage = (time) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleProtection = (status, time = null) => async (dispatch) => {
|
||||
dispatch(toggleProtectionRequest());
|
||||
try {
|
||||
const successMessage = status ? getDisabledMessage(time) : 'enabled_protection';
|
||||
await apiClient.setProtection({ enabled: !status, duration: time });
|
||||
dispatch(addSuccessToast(successMessage));
|
||||
dispatch(toggleProtectionSuccess({ disabledDuration: time }));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleProtectionFailure());
|
||||
}
|
||||
};
|
||||
export const toggleProtection =
|
||||
(status: any, time = null) =>
|
||||
async (dispatch: any) => {
|
||||
dispatch(toggleProtectionRequest());
|
||||
try {
|
||||
const successMessage = status ? getDisabledMessage(time) : 'enabled_protection';
|
||||
await apiClient.setProtection({ enabled: !status, duration: time });
|
||||
dispatch(addSuccessToast(successMessage));
|
||||
dispatch(toggleProtectionSuccess({ disabledDuration: time }));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleProtectionFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const setDisableDurationTime = createAction('SET_DISABLED_DURATION_TIME');
|
||||
|
||||
export const setProtectionTimerTime = (updatedTime) => async (dispatch) => {
|
||||
export const setProtectionTimerTime = (updatedTime: any) => async (dispatch: any) => {
|
||||
dispatch(setDisableDurationTime({ timeToEnableProtection: updatedTime }));
|
||||
};
|
||||
|
||||
@ -168,40 +164,42 @@ export const getVersionRequest = createAction('GET_VERSION_REQUEST');
|
||||
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
|
||||
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
|
||||
|
||||
export const getVersion = (recheck = false) => async (dispatch, getState) => {
|
||||
dispatch(getVersionRequest());
|
||||
try {
|
||||
const data = await apiClient.getGlobalVersion({ recheck_now: recheck });
|
||||
dispatch(getVersionSuccess(data));
|
||||
export const getVersion =
|
||||
(recheck = false) =>
|
||||
async (dispatch: any, getState: any) => {
|
||||
dispatch(getVersionRequest());
|
||||
try {
|
||||
const data = await apiClient.getGlobalVersion({ recheck_now: recheck });
|
||||
dispatch(getVersionSuccess(data));
|
||||
|
||||
if (recheck) {
|
||||
const { dnsVersion } = getState().dashboard;
|
||||
const currentVersion = dnsVersion === 'undefined' ? 0 : dnsVersion;
|
||||
if (recheck) {
|
||||
const { dnsVersion } = getState().dashboard;
|
||||
const currentVersion = dnsVersion === 'undefined' ? 0 : dnsVersion;
|
||||
|
||||
if (data && !areEqualVersions(currentVersion, data.new_version)) {
|
||||
dispatch(addSuccessToast('updates_checked'));
|
||||
} else {
|
||||
dispatch(addSuccessToast('updates_version_equal'));
|
||||
if (data && !areEqualVersions(currentVersion, data.new_version)) {
|
||||
dispatch(addSuccessToast('updates_checked'));
|
||||
} else {
|
||||
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 getUpdateFailure = createAction('GET_UPDATE_FAILURE');
|
||||
export const getUpdateSuccess = createAction('GET_UPDATE_SUCCESS');
|
||||
|
||||
const checkStatus = async (handleRequestSuccess, handleRequestError, attempts = 60) => {
|
||||
const checkStatus = async (handleRequestSuccess: any, handleRequestError: any, attempts = 60) => {
|
||||
let timeout;
|
||||
|
||||
if (attempts === 0) {
|
||||
handleRequestError();
|
||||
}
|
||||
|
||||
const rmTimeout = (t) => t && clearTimeout(t);
|
||||
const rmTimeout = (t: any) => t && clearTimeout(t);
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${apiClient.baseUrl}/status`);
|
||||
@ -220,25 +218,18 @@ const checkStatus = async (handleRequestSuccess, handleRequestError, attempts =
|
||||
}
|
||||
} catch (error) {
|
||||
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, getState) => {
|
||||
export const getUpdate = () => async (dispatch: any, getState: any) => {
|
||||
const { dnsVersion } = getState().dashboard;
|
||||
|
||||
dispatch(getUpdateRequest());
|
||||
const handleRequestError = () => {
|
||||
const options = {
|
||||
components: {
|
||||
a: <a href={MANUAL_UPDATE_LINK} target="_blank"
|
||||
rel="noopener noreferrer" />,
|
||||
a: <a href={MANUAL_UPDATE_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||
},
|
||||
};
|
||||
|
||||
@ -246,12 +237,13 @@ export const getUpdate = () => async (dispatch, getState) => {
|
||||
dispatch(getUpdateFailure());
|
||||
};
|
||||
|
||||
const handleRequestSuccess = (response) => {
|
||||
const handleRequestSuccess = (response: any) => {
|
||||
const responseVersion = response.data?.version;
|
||||
|
||||
if (dnsVersion !== responseVersion) {
|
||||
dispatch(getUpdateSuccess());
|
||||
window.location.reload(true);
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
@ -267,18 +259,20 @@ export const getClientsRequest = createAction('GET_CLIENTS_REQUEST');
|
||||
export const getClientsFailure = createAction('GET_CLIENTS_FAILURE');
|
||||
export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
||||
|
||||
export const getClients = () => async (dispatch) => {
|
||||
export const getClients = () => async (dispatch: any) => {
|
||||
dispatch(getClientsRequest());
|
||||
try {
|
||||
const data = await apiClient.getClients();
|
||||
const sortedClients = data.clients && sortClients(data.clients);
|
||||
const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients);
|
||||
|
||||
dispatch(getClientsSuccess({
|
||||
clients: sortedClients || [],
|
||||
autoClients: sortedAutoClients || [],
|
||||
supportedTags: data.supported_tags || [],
|
||||
}));
|
||||
dispatch(
|
||||
getClientsSuccess({
|
||||
clients: sortedClients || [],
|
||||
autoClients: sortedAutoClients || [],
|
||||
supportedTags: data.supported_tags || [],
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getClientsFailure());
|
||||
@ -289,7 +283,7 @@ export const getProfileRequest = createAction('GET_PROFILE_REQUEST');
|
||||
export const getProfileFailure = createAction('GET_PROFILE_FAILURE');
|
||||
export const getProfileSuccess = createAction('GET_PROFILE_SUCCESS');
|
||||
|
||||
export const getProfile = () => async (dispatch) => {
|
||||
export const getProfile = () => async (dispatch: any) => {
|
||||
dispatch(getProfileRequest());
|
||||
try {
|
||||
const profile = await apiClient.getProfile();
|
||||
@ -305,16 +299,17 @@ export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
||||
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
||||
export const setDnsRunningStatus = createAction('SET_DNS_RUNNING_STATUS');
|
||||
|
||||
export const getDnsStatus = () => async (dispatch) => {
|
||||
export const getDnsStatus = () => async (dispatch: any) => {
|
||||
dispatch(dnsStatusRequest());
|
||||
|
||||
const handleRequestError = () => {
|
||||
dispatch(addErrorToast({ error: 'dns_status_error' }));
|
||||
dispatch(dnsStatusFailure());
|
||||
window.location.reload(true);
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleRequestSuccess = (response) => {
|
||||
const handleRequestSuccess = (response: any) => {
|
||||
const dnsStatus = response.data;
|
||||
if (dnsStatus.protection_disabled_duration === 0) {
|
||||
dnsStatus.protection_disabled_duration = null;
|
||||
@ -342,16 +337,17 @@ export const timerStatusRequest = createAction('TIMER_STATUS_REQUEST');
|
||||
export const timerStatusFailure = createAction('TIMER_STATUS_FAILURE');
|
||||
export const timerStatusSuccess = createAction('TIMER_STATUS_SUCCESS');
|
||||
|
||||
export const getTimerStatus = () => async (dispatch) => {
|
||||
export const getTimerStatus = () => async (dispatch: any) => {
|
||||
dispatch(timerStatusRequest());
|
||||
|
||||
const handleRequestError = () => {
|
||||
dispatch(addErrorToast({ error: 'dns_status_error' }));
|
||||
dispatch(dnsStatusFailure());
|
||||
window.location.reload(true);
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleRequestSuccess = (response) => {
|
||||
const handleRequestSuccess = (response: any) => {
|
||||
const dnsStatus = response.data;
|
||||
if (dnsStatus.protection_disabled_duration === 0) {
|
||||
dnsStatus.protection_disabled_duration = null;
|
||||
@ -376,30 +372,26 @@ export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
|
||||
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
|
||||
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
|
||||
|
||||
export const testUpstream = (
|
||||
{
|
||||
bootstrap_dns,
|
||||
upstream_dns,
|
||||
local_ptr_upstreams,
|
||||
fallback_dns,
|
||||
}, upstream_dns_file,
|
||||
) => async (dispatch) => {
|
||||
dispatch(testUpstreamRequest());
|
||||
try {
|
||||
const removeComments = compose(filterOutComments, splitByNewLine);
|
||||
export const testUpstream =
|
||||
({ bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns }: any, upstream_dns_file: any) =>
|
||||
async (dispatch: any) => {
|
||||
dispatch(testUpstreamRequest());
|
||||
try {
|
||||
const removeComments = compose(filterOutComments, splitByNewLine);
|
||||
|
||||
const config = {
|
||||
bootstrap_dns: splitByNewLine(bootstrap_dns),
|
||||
private_upstream: splitByNewLine(local_ptr_upstreams),
|
||||
fallback_dns: splitByNewLine(fallback_dns),
|
||||
...(upstream_dns_file ? null : {
|
||||
upstream_dns: removeComments(upstream_dns),
|
||||
}),
|
||||
};
|
||||
const config = {
|
||||
bootstrap_dns: splitByNewLine(bootstrap_dns),
|
||||
private_upstream: splitByNewLine(local_ptr_upstreams),
|
||||
fallback_dns: splitByNewLine(fallback_dns),
|
||||
...(upstream_dns_file
|
||||
? null
|
||||
: {
|
||||
upstream_dns: removeComments(upstream_dns),
|
||||
}),
|
||||
};
|
||||
|
||||
const upstreamResponse = await apiClient.testUpstream(config);
|
||||
const testMessages = Object.keys(upstreamResponse)
|
||||
.map((key) => {
|
||||
const upstreamResponse = await apiClient.testUpstream(config);
|
||||
const testMessages = Object.keys(upstreamResponse).map((key) => {
|
||||
const message = upstreamResponse[key];
|
||||
if (message.startsWith('WARNING:')) {
|
||||
dispatch(addErrorToast({ error: i18next.t('dns_test_warning_toast', { key }) }));
|
||||
@ -407,46 +399,54 @@ export const testUpstream = (
|
||||
const info = message.substring(0, message.indexOf(':'));
|
||||
const [sectionKey, line] = info.split(' ');
|
||||
const section = i18next.t(sectionKey);
|
||||
dispatch(addErrorToast({ error: i18next.t('dns_test_parsing_error_toast', { section, line }) }));
|
||||
dispatch(
|
||||
addErrorToast({
|
||||
error: i18next.t('dns_test_parsing_error_toast', {
|
||||
section,
|
||||
line,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
} else if (message !== 'OK') {
|
||||
dispatch(addErrorToast({ error: i18next.t('dns_test_not_ok_toast', { key }) }));
|
||||
}
|
||||
return message;
|
||||
});
|
||||
|
||||
if (testMessages.every((message) => message === 'OK' || message.startsWith('WARNING:'))) {
|
||||
dispatch(addSuccessToast('dns_test_ok_toast'));
|
||||
if (testMessages.every((message) => message === 'OK' || message.startsWith('WARNING:'))) {
|
||||
dispatch(addSuccessToast('dns_test_ok_toast'));
|
||||
}
|
||||
|
||||
dispatch(testUpstreamSuccess());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(testUpstreamFailure());
|
||||
}
|
||||
};
|
||||
|
||||
dispatch(testUpstreamSuccess());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(testUpstreamFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const testUpstreamWithFormValues = () => async (dispatch, getState) => {
|
||||
export const testUpstreamWithFormValues = () => async (dispatch: any, getState: any) => {
|
||||
const { upstream_dns_file } = getState().dnsConfig;
|
||||
const {
|
||||
bootstrap_dns,
|
||||
upstream_dns,
|
||||
local_ptr_upstreams,
|
||||
fallback_dns,
|
||||
} = getState().form[FORM_NAME.UPSTREAM].values;
|
||||
const { bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns } =
|
||||
getState().form[FORM_NAME.UPSTREAM].values;
|
||||
|
||||
return dispatch(testUpstream({
|
||||
bootstrap_dns,
|
||||
upstream_dns,
|
||||
local_ptr_upstreams,
|
||||
fallback_dns,
|
||||
}, upstream_dns_file));
|
||||
return dispatch(
|
||||
testUpstream(
|
||||
{
|
||||
bootstrap_dns,
|
||||
upstream_dns,
|
||||
local_ptr_upstreams,
|
||||
fallback_dns,
|
||||
},
|
||||
upstream_dns_file,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const changeLanguageRequest = createAction('CHANGE_LANGUAGE_REQUEST');
|
||||
export const changeLanguageFailure = createAction('CHANGE_LANGUAGE_FAILURE');
|
||||
export const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS');
|
||||
|
||||
export const changeLanguage = (lang) => async (dispatch) => {
|
||||
export const changeLanguage = (lang: any) => async (dispatch: any) => {
|
||||
dispatch(changeLanguageRequest());
|
||||
try {
|
||||
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 changeThemeSuccess = createAction('CHANGE_THEME_SUCCESS');
|
||||
|
||||
export const changeTheme = (theme) => async (dispatch) => {
|
||||
export const changeTheme = (theme: any) => async (dispatch: any) => {
|
||||
dispatch(changeThemeRequest());
|
||||
try {
|
||||
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 getDhcpStatusFailure = createAction('GET_DHCP_STATUS_FAILURE');
|
||||
|
||||
export const getDhcpStatus = () => async (dispatch) => {
|
||||
export const getDhcpStatus = () => async (dispatch: any) => {
|
||||
dispatch(getDhcpStatusRequest());
|
||||
try {
|
||||
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 getDhcpInterfacesFailure = createAction('GET_DHCP_INTERFACES_FAILURE');
|
||||
|
||||
export const getDhcpInterfaces = () => async (dispatch) => {
|
||||
export const getDhcpInterfaces = () => async (dispatch: any) => {
|
||||
dispatch(getDhcpInterfacesRequest());
|
||||
try {
|
||||
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 findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
|
||||
|
||||
export const findActiveDhcp = (name) => async (dispatch, getState) => {
|
||||
export const findActiveDhcp = (name: any) => async (dispatch: any, getState: any) => {
|
||||
dispatch(findActiveDhcpRequest());
|
||||
try {
|
||||
const req = {
|
||||
@ -559,12 +559,12 @@ export const findActiveDhcp = (name) => async (dispatch, getState) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((hasV4Interface && v4.other_server.found === STATUS_RESPONSE.YES)
|
||||
|| (hasV6Interface && v6.other_server.found === STATUS_RESPONSE.YES)) {
|
||||
if (
|
||||
(hasV4Interface && v4.other_server.found === STATUS_RESPONSE.YES) ||
|
||||
(hasV6Interface && v6.other_server.found === STATUS_RESPONSE.YES)
|
||||
) {
|
||||
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', {
|
||||
interfaceName: interface_name,
|
||||
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 setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');
|
||||
|
||||
export const setDhcpConfig = (values) => async (dispatch) => {
|
||||
export const setDhcpConfig = (values: any) => async (dispatch: any) => {
|
||||
dispatch(setDhcpConfigRequest());
|
||||
try {
|
||||
await apiClient.setDhcpConfig(values);
|
||||
@ -603,7 +603,7 @@ export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST');
|
||||
export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE');
|
||||
export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS');
|
||||
|
||||
export const toggleDhcp = (values) => async (dispatch) => {
|
||||
export const toggleDhcp = (values: any) => async (dispatch: any) => {
|
||||
dispatch(toggleDhcpRequest());
|
||||
let config = {
|
||||
...values,
|
||||
@ -633,7 +633,7 @@ export const resetDhcpRequest = createAction('RESET_DHCP_REQUEST');
|
||||
export const resetDhcpSuccess = createAction('RESET_DHCP_SUCCESS');
|
||||
export const resetDhcpFailure = createAction('RESET_DHCP_FAILURE');
|
||||
|
||||
export const resetDhcp = () => async (dispatch) => {
|
||||
export const resetDhcp = () => async (dispatch: any) => {
|
||||
dispatch(resetDhcpRequest());
|
||||
try {
|
||||
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 resetDhcpLeasesFailure = createAction('RESET_DHCP_LEASES_FAILURE');
|
||||
|
||||
export const resetDhcpLeases = () => async (dispatch) => {
|
||||
export const resetDhcpLeases = () => async (dispatch: any) => {
|
||||
dispatch(resetDhcpLeasesRequest());
|
||||
try {
|
||||
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 addStaticLeaseSuccess = createAction('ADD_STATIC_LEASE_SUCCESS');
|
||||
|
||||
export const addStaticLease = (config) => async (dispatch) => {
|
||||
export const addStaticLease = (config: any) => async (dispatch: any) => {
|
||||
dispatch(addStaticLeaseRequest());
|
||||
try {
|
||||
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 removeStaticLeaseSuccess = createAction('REMOVE_STATIC_LEASE_SUCCESS');
|
||||
|
||||
export const removeStaticLease = (config) => async (dispatch) => {
|
||||
export const removeStaticLease = (config: any) => async (dispatch: any) => {
|
||||
dispatch(removeStaticLeaseRequest());
|
||||
try {
|
||||
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 updateStaticLeaseSuccess = createAction('UPDATE_STATIC_LEASE_SUCCESS');
|
||||
|
||||
export const updateStaticLease = (config) => async (dispatch) => {
|
||||
export const updateStaticLease = (config: any) => async (dispatch: any) => {
|
||||
dispatch(updateStaticLeaseRequest());
|
||||
try {
|
||||
await apiClient.updateStaticLease(config);
|
||||
@ -719,42 +719,42 @@ export const updateStaticLease = (config) => async (dispatch) => {
|
||||
|
||||
export const removeToast = createAction('REMOVE_TOAST');
|
||||
|
||||
export const toggleBlocking = (
|
||||
type, domain, baseRule, baseUnblocking,
|
||||
) => async (dispatch, getState) => {
|
||||
const baseBlockingRule = baseRule || `||${domain}^$important`;
|
||||
const baseUnblockingRule = baseUnblocking || `@@${baseBlockingRule}`;
|
||||
const { userRules } = getState().filtering;
|
||||
export const toggleBlocking =
|
||||
(type: any, domain: any, baseRule?: string, baseUnblocking?: string) => async (dispatch: any, getState: any) => {
|
||||
const baseBlockingRule = baseRule || `||${domain}^$important`;
|
||||
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 unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseBlockingRule : baseUnblockingRule;
|
||||
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
||||
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
||||
const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblockingRule : baseBlockingRule;
|
||||
const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseBlockingRule : baseUnblockingRule;
|
||||
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
||||
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
||||
|
||||
const matchPreparedBlockingRule = userRules.match(preparedBlockingRule);
|
||||
const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule);
|
||||
const matchPreparedBlockingRule = userRules.match(preparedBlockingRule);
|
||||
const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule);
|
||||
|
||||
if (matchPreparedBlockingRule) {
|
||||
await dispatch(setRules(userRules.replace(`${blockingRule}`, '')));
|
||||
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
|
||||
} else if (!matchPreparedUnblockingRule) {
|
||||
await dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`));
|
||||
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
|
||||
} else if (matchPreparedUnblockingRule) {
|
||||
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
|
||||
return;
|
||||
} else if (!matchPreparedBlockingRule) {
|
||||
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
|
||||
return;
|
||||
}
|
||||
if (matchPreparedBlockingRule) {
|
||||
await dispatch(setRules(userRules.replace(`${blockingRule}`, '')));
|
||||
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
|
||||
} else if (!matchPreparedUnblockingRule) {
|
||||
await dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`));
|
||||
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
|
||||
} else if (matchPreparedUnblockingRule) {
|
||||
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
|
||||
return;
|
||||
} else if (!matchPreparedBlockingRule) {
|
||||
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(getFilteringStatus());
|
||||
};
|
||||
dispatch(getFilteringStatus());
|
||||
};
|
||||
|
||||
export const toggleBlockingForClient = (type, domain, client) => {
|
||||
const escapedClientName = client.replace(/'/g, '\\\'')
|
||||
export const toggleBlockingForClient = (type: any, domain: any, client: any) => {
|
||||
const escapedClientName = client
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/,/g, '\\,')
|
||||
.replace(/\|/g, '\\|');
|
@ -9,7 +9,7 @@ export const getDefaultAddressesRequest = createAction('GET_DEFAULT_ADDRESSES_RE
|
||||
export const getDefaultAddressesFailure = createAction('GET_DEFAULT_ADDRESSES_FAILURE');
|
||||
export const getDefaultAddressesSuccess = createAction('GET_DEFAULT_ADDRESSES_SUCCESS');
|
||||
|
||||
export const getDefaultAddresses = () => async (dispatch) => {
|
||||
export const getDefaultAddresses = () => async (dispatch: any) => {
|
||||
dispatch(getDefaultAddressesRequest());
|
||||
try {
|
||||
const addresses = await apiClient.getDefaultAddresses();
|
||||
@ -24,13 +24,10 @@ export const setAllSettingsRequest = createAction('SET_ALL_SETTINGS_REQUEST');
|
||||
export const setAllSettingsFailure = createAction('SET_ALL_SETTINGS_FAILURE');
|
||||
export const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS');
|
||||
|
||||
export const setAllSettings = (values) => async (dispatch) => {
|
||||
export const setAllSettings = (values: any) => async (dispatch: any) => {
|
||||
dispatch(setAllSettingsRequest());
|
||||
try {
|
||||
const {
|
||||
confirm_password,
|
||||
...config
|
||||
} = values;
|
||||
const { confirm_password, ...config } = values;
|
||||
|
||||
await apiClient.setAllSettings(config);
|
||||
dispatch(setAllSettingsSuccess());
|
||||
@ -47,7 +44,7 @@ export const checkConfigRequest = createAction('CHECK_CONFIG_REQUEST');
|
||||
export const checkConfigFailure = createAction('CHECK_CONFIG_FAILURE');
|
||||
export const checkConfigSuccess = createAction('CHECK_CONFIG_SUCCESS');
|
||||
|
||||
export const checkConfig = (values) => async (dispatch) => {
|
||||
export const checkConfig = (values: any) => async (dispatch: any) => {
|
||||
dispatch(checkConfigRequest());
|
||||
try {
|
||||
const check = await apiClient.checkConfig(values);
|
@ -8,12 +8,12 @@ export const processLoginRequest = createAction('PROCESS_LOGIN_REQUEST');
|
||||
export const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE');
|
||||
export const processLoginSuccess = createAction('PROCESS_LOGIN_SUCCESS');
|
||||
|
||||
export const processLogin = (values) => async (dispatch) => {
|
||||
export const processLogin = (values: any) => async (dispatch: any) => {
|
||||
dispatch(processLoginRequest());
|
||||
try {
|
||||
await apiClient.login(values);
|
||||
const dashboardUrl = window.location.origin
|
||||
+ window.location.pathname.replace(HTML_PAGES.LOGIN, HTML_PAGES.MAIN);
|
||||
const dashboardUrl =
|
||||
window.location.origin + window.location.pathname.replace(HTML_PAGES.LOGIN, HTML_PAGES.MAIN);
|
||||
window.location.replace(dashboardUrl);
|
||||
dispatch(processLoginSuccess());
|
||||
} catch (error) {
|
@ -1,13 +1,12 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
|
||||
import apiClient from '../api/Api';
|
||||
|
||||
import { normalizeLogs } from '../helpers/helpers';
|
||||
import {
|
||||
DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT,
|
||||
} from '../helpers/constants';
|
||||
import { DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT } from '../helpers/constants';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
|
||||
const getLogsWithParams = async (config) => {
|
||||
const getLogsWithParams = async (config: any) => {
|
||||
const { older_than, filter, ...values } = config;
|
||||
const rawLogs = await apiClient.getQueryLog({
|
||||
...filter,
|
||||
@ -28,20 +27,20 @@ export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUES
|
||||
export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');
|
||||
export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS');
|
||||
|
||||
const shortPollQueryLogs = async (data, filter, dispatch, getState, total) => {
|
||||
const shortPollQueryLogs = async (data: any, filter: any, dispatch: any, getState: any, total?: any) => {
|
||||
const { logs, oldest } = data;
|
||||
const totalData = total || { logs };
|
||||
|
||||
const queryForm = getState().form[FORM_NAME.LOGS_FILTER];
|
||||
const currentQuery = queryForm && queryForm.values.search;
|
||||
const previousQuery = filter?.search;
|
||||
const isQueryTheSame = typeof previousQuery === 'string'
|
||||
&& typeof currentQuery === 'string'
|
||||
&& previousQuery === currentQuery;
|
||||
const isQueryTheSame =
|
||||
typeof previousQuery === 'string' && typeof currentQuery === 'string' && previousQuery === currentQuery;
|
||||
|
||||
const isShortPollingNeeded = (logs.length < QUERY_LOGS_PAGE_LIMIT
|
||||
|| totalData.logs.length < QUERY_LOGS_PAGE_LIMIT)
|
||||
&& oldest !== '' && isQueryTheSame;
|
||||
const isShortPollingNeeded =
|
||||
(logs.length < QUERY_LOGS_PAGE_LIMIT || totalData.logs.length < QUERY_LOGS_PAGE_LIMIT) &&
|
||||
oldest !== '' &&
|
||||
isQueryTheSame;
|
||||
|
||||
if (isShortPollingNeeded) {
|
||||
dispatch(getAdditionalLogsRequest());
|
||||
@ -75,22 +74,24 @@ export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
||||
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
||||
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
||||
|
||||
export const updateLogs = () => async (dispatch, getState) => {
|
||||
export const updateLogs = () => async (dispatch: any, getState: any) => {
|
||||
try {
|
||||
const { logs, oldest, older_than } = getState().queryLogs;
|
||||
|
||||
dispatch(getLogsSuccess({
|
||||
logs,
|
||||
oldest,
|
||||
older_than,
|
||||
}));
|
||||
dispatch(
|
||||
getLogsSuccess({
|
||||
logs,
|
||||
oldest,
|
||||
older_than,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getLogsFailure(error));
|
||||
}
|
||||
};
|
||||
|
||||
export const getLogs = () => async (dispatch, getState) => {
|
||||
export const getLogs = () => async (dispatch: any, getState: any) => {
|
||||
dispatch(getLogsRequest());
|
||||
try {
|
||||
const { isFiltered, filter, oldest } = getState().queryLogs;
|
||||
@ -121,26 +122,29 @@ export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
|
||||
* @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object
|
||||
* @returns function
|
||||
*/
|
||||
export const setLogsFilter = (filter) => setLogsFilterRequest(filter);
|
||||
export const setLogsFilter = (filter: any) => setLogsFilterRequest(filter);
|
||||
|
||||
export const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST');
|
||||
export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');
|
||||
export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');
|
||||
|
||||
export const setFilteredLogs = (filter) => async (dispatch, getState) => {
|
||||
export const setFilteredLogs = (filter?: any) => async (dispatch: any, getState: any) => {
|
||||
dispatch(setFilteredLogsRequest());
|
||||
try {
|
||||
const data = await getLogsWithParams({
|
||||
older_than: '',
|
||||
filter,
|
||||
});
|
||||
|
||||
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
|
||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||
|
||||
dispatch(setFilteredLogsSuccess({
|
||||
...updatedData,
|
||||
filter,
|
||||
}));
|
||||
dispatch(
|
||||
setFilteredLogsSuccess({
|
||||
...updatedData,
|
||||
filter,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setFilteredLogsFailure(error));
|
||||
@ -149,7 +153,7 @@ export const setFilteredLogs = (filter) => async (dispatch, getState) => {
|
||||
|
||||
export const resetFilteredLogs = () => setFilteredLogs(DEFAULT_LOGS_FILTER);
|
||||
|
||||
export const refreshFilteredLogs = () => async (dispatch, getState) => {
|
||||
export const refreshFilteredLogs = () => async (dispatch: any, getState: any) => {
|
||||
const { filter } = getState().queryLogs;
|
||||
await dispatch(setFilteredLogs(filter));
|
||||
};
|
||||
@ -158,7 +162,7 @@ export const clearLogsRequest = createAction('CLEAR_LOGS_REQUEST');
|
||||
export const clearLogsFailure = createAction('CLEAR_LOGS_FAILURE');
|
||||
export const clearLogsSuccess = createAction('CLEAR_LOGS_SUCCESS');
|
||||
|
||||
export const clearLogs = () => async (dispatch) => {
|
||||
export const clearLogs = () => async (dispatch: any) => {
|
||||
dispatch(clearLogsRequest());
|
||||
try {
|
||||
await apiClient.clearQueryLog();
|
||||
@ -174,7 +178,7 @@ export const getLogsConfigRequest = createAction('GET_LOGS_CONFIG_REQUEST');
|
||||
export const getLogsConfigFailure = createAction('GET_LOGS_CONFIG_FAILURE');
|
||||
export const getLogsConfigSuccess = createAction('GET_LOGS_CONFIG_SUCCESS');
|
||||
|
||||
export const getLogsConfig = () => async (dispatch) => {
|
||||
export const getLogsConfig = () => async (dispatch: any) => {
|
||||
dispatch(getLogsConfigRequest());
|
||||
try {
|
||||
const data = await apiClient.getQueryLogConfig();
|
||||
@ -189,7 +193,7 @@ export const setLogsConfigRequest = createAction('SET_LOGS_CONFIG_REQUEST');
|
||||
export const setLogsConfigFailure = createAction('SET_LOGS_CONFIG_FAILURE');
|
||||
export const setLogsConfigSuccess = createAction('SET_LOGS_CONFIG_SUCCESS');
|
||||
|
||||
export const setLogsConfig = (config) => async (dispatch) => {
|
||||
export const setLogsConfig = (config: any) => async (dispatch: any) => {
|
||||
dispatch(setLogsConfigRequest());
|
||||
try {
|
||||
await apiClient.setQueryLogConfig(config);
|
@ -9,7 +9,7 @@ export const getRewritesListRequest = createAction('GET_REWRITES_LIST_REQUEST');
|
||||
export const getRewritesListFailure = createAction('GET_REWRITES_LIST_FAILURE');
|
||||
export const getRewritesListSuccess = createAction('GET_REWRITES_LIST_SUCCESS');
|
||||
|
||||
export const getRewritesList = () => async (dispatch) => {
|
||||
export const getRewritesList = () => async (dispatch: any) => {
|
||||
dispatch(getRewritesListRequest());
|
||||
try {
|
||||
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 addRewriteSuccess = createAction('ADD_REWRITE_SUCCESS');
|
||||
|
||||
export const addRewrite = (config) => async (dispatch) => {
|
||||
export const addRewrite = (config: any) => async (dispatch: any) => {
|
||||
dispatch(addRewriteRequest());
|
||||
try {
|
||||
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.update - updated DNS rewrite value
|
||||
*/
|
||||
export const updateRewrite = (config) => async (dispatch) => {
|
||||
export const updateRewrite = (config: any) => async (dispatch: any) => {
|
||||
dispatch(updateRewriteRequest());
|
||||
try {
|
||||
await apiClient.updateRewrite(config);
|
||||
@ -65,7 +65,7 @@ export const deleteRewriteRequest = createAction('DELETE_REWRITE_REQUEST');
|
||||
export const deleteRewriteFailure = createAction('DELETE_REWRITE_FAILURE');
|
||||
export const deleteRewriteSuccess = createAction('DELETE_REWRITE_SUCCESS');
|
||||
|
||||
export const deleteRewrite = (config) => async (dispatch) => {
|
||||
export const deleteRewrite = (config: any) => async (dispatch: any) => {
|
||||
dispatch(deleteRewriteRequest());
|
||||
try {
|
||||
await apiClient.deleteRewrite(config);
|
@ -6,7 +6,7 @@ export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQU
|
||||
export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');
|
||||
export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');
|
||||
|
||||
export const getBlockedServices = () => async (dispatch) => {
|
||||
export const getBlockedServices = () => async (dispatch: any) => {
|
||||
dispatch(getBlockedServicesRequest());
|
||||
try {
|
||||
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 getAllBlockedServicesSuccess = createAction('GET_ALL_BLOCKED_SERVICES_SUCCESS');
|
||||
|
||||
export const getAllBlockedServices = () => async (dispatch) => {
|
||||
export const getAllBlockedServices = () => async (dispatch: any) => {
|
||||
dispatch(getAllBlockedServicesRequest());
|
||||
try {
|
||||
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 updateBlockedServicesSuccess = createAction('UPDATE_BLOCKED_SERVICES_SUCCESS');
|
||||
|
||||
export const updateBlockedServices = (values) => async (dispatch) => {
|
||||
export const updateBlockedServices = (values: any) => async (dispatch: any) => {
|
||||
dispatch(updateBlockedServicesRequest());
|
||||
try {
|
||||
await apiClient.updateBlockedServices(values);
|
@ -1,16 +1,14 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
|
||||
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';
|
||||
|
||||
export const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST');
|
||||
export const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE');
|
||||
export const getStatsConfigSuccess = createAction('GET_STATS_CONFIG_SUCCESS');
|
||||
|
||||
export const getStatsConfig = () => async (dispatch) => {
|
||||
export const getStatsConfig = () => async (dispatch: any) => {
|
||||
dispatch(getStatsConfigRequest());
|
||||
try {
|
||||
const data = await apiClient.getStatsConfig();
|
||||
@ -25,7 +23,7 @@ export const setStatsConfigRequest = createAction('SET_STATS_CONFIG_REQUEST');
|
||||
export const setStatsConfigFailure = createAction('SET_STATS_CONFIG_FAILURE');
|
||||
export const setStatsConfigSuccess = createAction('SET_STATS_CONFIG_SUCCESS');
|
||||
|
||||
export const setStatsConfig = (config) => async (dispatch) => {
|
||||
export const setStatsConfig = (config: any) => async (dispatch: any) => {
|
||||
dispatch(setStatsConfigRequest());
|
||||
try {
|
||||
await apiClient.setStatsConfig(config);
|
||||
@ -41,11 +39,12 @@ export const getStatsRequest = createAction('GET_STATS_REQUEST');
|
||||
export const getStatsFailure = createAction('GET_STATS_FAILURE');
|
||||
export const getStatsSuccess = createAction('GET_STATS_SUCCESS');
|
||||
|
||||
export const getStats = () => async (dispatch) => {
|
||||
export const getStats = () => async (dispatch: any) => {
|
||||
dispatch(getStatsRequest());
|
||||
try {
|
||||
const stats = await apiClient.getStats();
|
||||
const normalizedTopClients = normalizeTopStats(stats.top_clients);
|
||||
|
||||
const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name');
|
||||
const clients = await apiClient.findClients(clientsParams);
|
||||
const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name');
|
||||
@ -71,7 +70,7 @@ export const resetStatsRequest = createAction('RESET_STATS_REQUEST');
|
||||
export const resetStatsFailure = createAction('RESET_STATS_FAILURE');
|
||||
export const resetStatsSuccess = createAction('RESET_STATS_SUCCESS');
|
||||
|
||||
export const resetStats = () => async (dispatch) => {
|
||||
export const resetStats = () => async (dispatch: any) => {
|
||||
dispatch(getStatsRequest());
|
||||
try {
|
||||
await apiClient.resetStats();
|
@ -1,17 +1,16 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { getPathWithQueryString } from '../helpers/helpers';
|
||||
import {
|
||||
QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES,
|
||||
} from '../helpers/constants';
|
||||
import { BASE_URL } from '../../constants';
|
||||
|
||||
import { getPathWithQueryString } from '../helpers/helpers';
|
||||
import { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES } from '../helpers/constants';
|
||||
import i18n from '../i18n';
|
||||
import { LANGUAGES } from '../helpers/twosky';
|
||||
|
||||
class Api {
|
||||
baseUrl = BASE_URL;
|
||||
|
||||
async makeRequest(path, method = 'POST', config) {
|
||||
async makeRequest(path: any, method = 'POST', config: any = {}) {
|
||||
const url = `${this.baseUrl}/${path}`;
|
||||
|
||||
const axiosConfig = config || {};
|
||||
@ -29,26 +28,26 @@ class Api {
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const errorPath = url;
|
||||
|
||||
if (error.response) {
|
||||
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) {
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(`${errorPath} | ${error.response.data} | ${error.response.status}`);
|
||||
}
|
||||
|
||||
throw new Error(`${errorPath} | ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Global methods
|
||||
GLOBAL_STATUS = { path: 'status', method: 'GET' }
|
||||
GLOBAL_STATUS = { path: 'status', method: 'GET' };
|
||||
|
||||
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
|
||||
|
||||
@ -58,10 +57,11 @@ class Api {
|
||||
|
||||
getGlobalStatus() {
|
||||
const { path, method } = this.GLOBAL_STATUS;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
testUpstream(servers) {
|
||||
testUpstream(servers: any) {
|
||||
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
|
||||
const config = {
|
||||
data: servers,
|
||||
@ -69,7 +69,7 @@ class Api {
|
||||
return this.makeRequest(path, method, config);
|
||||
}
|
||||
|
||||
getGlobalVersion(data) {
|
||||
getGlobalVersion(data: any) {
|
||||
const { path, method } = this.GLOBAL_VERSION;
|
||||
const config = {
|
||||
data,
|
||||
@ -79,6 +79,7 @@ class Api {
|
||||
|
||||
getUpdate() {
|
||||
const { path, method } = this.GLOBAL_UPDATE;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
@ -101,10 +102,11 @@ class Api {
|
||||
|
||||
getFilteringStatus() {
|
||||
const { path, method } = this.FILTERING_STATUS;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
refreshFilters(config) {
|
||||
refreshFilters(config: any) {
|
||||
const { path, method } = this.FILTERING_REFRESH;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -113,7 +115,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
addFilter(config) {
|
||||
addFilter(config: any) {
|
||||
const { path, method } = this.FILTERING_ADD_FILTER;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -122,7 +124,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
removeFilter(config) {
|
||||
removeFilter(config: any) {
|
||||
const { path, method } = this.FILTERING_REMOVE_FILTER;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -131,7 +133,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
setRules(rules) {
|
||||
setRules(rules: any) {
|
||||
const { path, method } = this.FILTERING_SET_RULES;
|
||||
const parameters = {
|
||||
data: rules,
|
||||
@ -139,7 +141,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
setFiltersConfig(config) {
|
||||
setFiltersConfig(config: any) {
|
||||
const { path, method } = this.FILTERING_CONFIG;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -147,7 +149,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
setFilterUrl(config) {
|
||||
setFilterUrl(config: any) {
|
||||
const { path, method } = this.FILTERING_SET_URL;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -155,9 +157,10 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
checkHost(params) {
|
||||
checkHost(params: any) {
|
||||
const { path, method } = this.FILTERING_CHECK_HOST;
|
||||
const url = getPathWithQueryString(path, params);
|
||||
|
||||
return this.makeRequest(url, method);
|
||||
}
|
||||
|
||||
@ -170,16 +173,19 @@ class Api {
|
||||
|
||||
getParentalStatus() {
|
||||
const { path, method } = this.PARENTAL_STATUS;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
enableParentalControl() {
|
||||
const { path, method } = this.PARENTAL_ENABLE;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
disableParentalControl() {
|
||||
const { path, method } = this.PARENTAL_DISABLE;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
@ -192,16 +198,19 @@ class Api {
|
||||
|
||||
getSafebrowsingStatus() {
|
||||
const { path, method } = this.SAFEBROWSING_STATUS;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
enableSafebrowsing() {
|
||||
const { path, method } = this.SAFEBROWSING_ENABLE;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
disableSafebrowsing() {
|
||||
const { path, method } = this.SAFEBROWSING_DISABLE;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
@ -212,6 +221,7 @@ class Api {
|
||||
|
||||
getSafesearchStatus() {
|
||||
const { path, method } = this.SAFESEARCH_STATUS;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
@ -228,7 +238,7 @@ class Api {
|
||||
* @param {*} data - SafeSearchConfig
|
||||
* @returns 200 ok
|
||||
*/
|
||||
updateSafesearch(data) {
|
||||
updateSafesearch(data: any) {
|
||||
const { path, method } = this.SAFESEARCH_UPDATE;
|
||||
return this.makeRequest(path, method, { data });
|
||||
}
|
||||
@ -245,7 +255,7 @@ class Api {
|
||||
|
||||
// Language
|
||||
|
||||
async changeLanguage(config) {
|
||||
async changeLanguage(config: any) {
|
||||
const profile = await this.getProfile();
|
||||
profile.language = config.language;
|
||||
|
||||
@ -254,7 +264,7 @@ class Api {
|
||||
|
||||
// Theme
|
||||
|
||||
async changeTheme(config) {
|
||||
async changeTheme(config: any) {
|
||||
const profile = await this.getProfile();
|
||||
profile.theme = config.theme;
|
||||
|
||||
@ -282,15 +292,17 @@ class Api {
|
||||
|
||||
getDhcpStatus() {
|
||||
const { path, method } = this.DHCP_STATUS;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
getDhcpInterfaces() {
|
||||
const { path, method } = this.DHCP_INTERFACES;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setDhcpConfig(config) {
|
||||
setDhcpConfig(config: any) {
|
||||
const { path, method } = this.DHCP_SET_CONFIG;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -298,7 +310,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
findActiveDhcp(req) {
|
||||
findActiveDhcp(req: any) {
|
||||
const { path, method } = this.DHCP_FIND_ACTIVE;
|
||||
const parameters = {
|
||||
data: req,
|
||||
@ -306,7 +318,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
addStaticLease(config) {
|
||||
addStaticLease(config: any) {
|
||||
const { path, method } = this.DHCP_ADD_STATIC_LEASE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -314,7 +326,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
removeStaticLease(config) {
|
||||
removeStaticLease(config: any) {
|
||||
const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -322,7 +334,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
updateStaticLease(config) {
|
||||
updateStaticLease(config: any) {
|
||||
const { path, method } = this.DHCP_UPDATE_STATIC_LEASE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -332,11 +344,13 @@ class Api {
|
||||
|
||||
resetDhcp() {
|
||||
const { path, method } = this.DHCP_RESET;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
resetDhcpLeases() {
|
||||
const { path, method } = this.DHCP_LEASES_RESET;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
@ -349,10 +363,11 @@ class Api {
|
||||
|
||||
getDefaultAddresses() {
|
||||
const { path, method } = this.INSTALL_GET_ADDRESSES;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setAllSettings(config) {
|
||||
setAllSettings(config: any) {
|
||||
const { path, method } = this.INSTALL_CONFIGURE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -360,7 +375,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
checkConfig(config) {
|
||||
checkConfig(config: any) {
|
||||
const { path, method } = this.INSTALL_CHECK_CONFIG;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -377,10 +392,11 @@ class Api {
|
||||
|
||||
getTlsStatus() {
|
||||
const { path, method } = this.TLS_STATUS;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setTlsConfig(config) {
|
||||
setTlsConfig(config: any) {
|
||||
const { path, method } = this.TLS_CONFIG;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -388,7 +404,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
validateTlsConfig(config) {
|
||||
validateTlsConfig(config: any) {
|
||||
const { path, method } = this.TLS_VALIDATE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -409,10 +425,11 @@ class Api {
|
||||
|
||||
getClients() {
|
||||
const { path, method } = this.GET_CLIENTS;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
addClient(config) {
|
||||
addClient(config: any) {
|
||||
const { path, method } = this.ADD_CLIENT;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -420,7 +437,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
deleteClient(config) {
|
||||
deleteClient(config: any) {
|
||||
const { path, method } = this.DELETE_CLIENT;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -428,7 +445,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
updateClient(config) {
|
||||
updateClient(config: any) {
|
||||
const { path, method } = this.UPDATE_CLIENT;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -436,9 +453,10 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
findClients(params) {
|
||||
findClients(params: any) {
|
||||
const { path, method } = this.FIND_CLIENTS;
|
||||
const url = getPathWithQueryString(path, params);
|
||||
|
||||
return this.makeRequest(url, method);
|
||||
}
|
||||
|
||||
@ -449,10 +467,11 @@ class Api {
|
||||
|
||||
getAccessList() {
|
||||
const { path, method } = this.ACCESS_LIST;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setAccessList(config) {
|
||||
setAccessList(config: any) {
|
||||
const { path, method } = this.ACCESS_SET;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -471,10 +490,11 @@ class Api {
|
||||
|
||||
getRewritesList() {
|
||||
const { path, method } = this.REWRITES_LIST;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
addRewrite(config) {
|
||||
addRewrite(config: any) {
|
||||
const { path, method } = this.REWRITE_ADD;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -482,7 +502,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
updateRewrite(config) {
|
||||
updateRewrite(config: any) {
|
||||
const { path, method } = this.REWRITE_UPDATE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -490,7 +510,7 @@ class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
deleteRewrite(config) {
|
||||
deleteRewrite(config: any) {
|
||||
const { path, method } = this.REWRITE_DELETE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -507,15 +527,17 @@ class Api {
|
||||
|
||||
getAllBlockedServices() {
|
||||
const { path, method } = this.BLOCKED_SERVICES_ALL;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
getBlockedServices() {
|
||||
const { path, method } = this.BLOCKED_SERVICES_GET;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
updateBlockedServices(config) {
|
||||
updateBlockedServices(config: any) {
|
||||
const { path, method } = this.BLOCKED_SERVICES_UPDATE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
@ -534,15 +556,17 @@ class Api {
|
||||
|
||||
getStats() {
|
||||
const { path, method } = this.GET_STATS;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
getStatsConfig() {
|
||||
const { path, method } = this.GET_STATS_CONFIG;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setStatsConfig(data) {
|
||||
setStatsConfig(data: any) {
|
||||
const { path, method } = this.UPDATE_STATS_CONFIG;
|
||||
const config = {
|
||||
data,
|
||||
@ -552,6 +576,7 @@ class Api {
|
||||
|
||||
resetStats() {
|
||||
const { path, method } = this.STATS_RESET;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
@ -564,20 +589,22 @@ class Api {
|
||||
|
||||
QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' };
|
||||
|
||||
getQueryLog(params) {
|
||||
getQueryLog(params: any) {
|
||||
const { path, method } = this.GET_QUERY_LOG;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
params.limit = QUERY_LOGS_PAGE_LIMIT;
|
||||
const url = getPathWithQueryString(path, params);
|
||||
|
||||
return this.makeRequest(url, method);
|
||||
}
|
||||
|
||||
getQueryLogConfig() {
|
||||
const { path, method } = this.GET_QUERY_LOG_CONFIG;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setQueryLogConfig(data) {
|
||||
setQueryLogConfig(data: any) {
|
||||
const { path, method } = this.UPDATE_QUERY_LOG_CONFIG;
|
||||
const config = {
|
||||
data,
|
||||
@ -587,13 +614,14 @@ class Api {
|
||||
|
||||
clearQueryLog() {
|
||||
const { path, method } = this.QUERY_LOG_CLEAR;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
// Login
|
||||
LOGIN = { path: 'login', method: 'POST' };
|
||||
|
||||
login(data) {
|
||||
login(data: any) {
|
||||
const { path, method } = this.LOGIN;
|
||||
const config = {
|
||||
data,
|
||||
@ -608,10 +636,11 @@ class Api {
|
||||
|
||||
getProfile() {
|
||||
const { path, method } = this.GET_PROFILE;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setProfile(data) {
|
||||
setProfile(data: any) {
|
||||
const theme = data.theme ? data.theme : THEMES.auto;
|
||||
const defaultLanguage = i18n.language ? i18n.language : LANGUAGES.en;
|
||||
const language = data.language ? data.language : defaultLanguage;
|
||||
@ -629,10 +658,11 @@ class Api {
|
||||
|
||||
getDnsConfig() {
|
||||
const { path, method } = this.GET_DNS_CONFIG;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setDnsConfig(data) {
|
||||
setDnsConfig(data: any) {
|
||||
const { path, method } = this.SET_DNS_CONFIG;
|
||||
const config = {
|
||||
data,
|
||||
@ -642,7 +672,7 @@ class Api {
|
||||
|
||||
SET_PROTECTION = { path: 'protection', method: 'POST' };
|
||||
|
||||
setProtection(data) {
|
||||
setProtection(data: any) {
|
||||
const { enabled, duration } = data;
|
||||
const { path, method } = this.SET_PROTECTION;
|
||||
|
||||
@ -654,6 +684,7 @@ class Api {
|
||||
|
||||
clearCache() {
|
||||
const { path, method } = this.CLEAR_CACHE;
|
||||
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
}
|
@ -15,8 +15,8 @@
|
||||
--btn-success-bgcolor: #5eba00;
|
||||
--form-disabled-bgcolor: #f8f9fa;
|
||||
--form-disabled-color: #495057;
|
||||
--rt-nodata-bgcolor: rgba(255,255,255,0.8);
|
||||
--rt-nodata-color: rgba(0,0,0,0.5);
|
||||
--rt-nodata-bgcolor: rgba(255, 255, 255, 0.8);
|
||||
--rt-nodata-color: rgba(0, 0, 0, 0.5);
|
||||
--modal-overlay-bgcolor: rgba(255, 255, 255, 0.75);
|
||||
--logs__table-bgcolor: #fff;
|
||||
--logs__row--blue-bgcolor: #e5effd;
|
||||
@ -28,7 +28,7 @@
|
||||
--gray-d8: #d8d8d8;
|
||||
--gray-f3: #f3f3f3;
|
||||
--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;
|
||||
--alert-message-color: #24426c;
|
||||
--alert-message-border: #cbdbf2;
|
||||
@ -37,7 +37,7 @@
|
||||
--radio-bg: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
[data-theme='dark'] {
|
||||
--black: #ffffff;
|
||||
--bgcolor: #131313;
|
||||
--mcolor: #e6e6e6;
|
||||
@ -74,12 +74,14 @@
|
||||
body {
|
||||
margin: 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 */
|
||||
@media screen and (max-width: 767px) {
|
||||
input, select, textarea {
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-size: var(--font-size-disable-autozoom);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { HashRouter, Route } from 'react-router-dom';
|
||||
import LoadingBar from 'react-redux-loading-bar';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
@ -9,8 +10,6 @@ import '../ui/ReactTable.css';
|
||||
import './index.css';
|
||||
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import propTypes from 'prop-types';
|
||||
import Toasts from '../Toasts';
|
||||
import Footer from '../ui/Footer';
|
||||
import Status from '../ui/Status';
|
||||
@ -19,15 +18,14 @@ import UpdateOverlay from '../ui/UpdateOverlay';
|
||||
import EncryptionTopline from '../ui/EncryptionTopline';
|
||||
import Icons from '../ui/Icons';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
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 Header from '../Header';
|
||||
|
||||
import { changeLanguage, getDnsStatus, getTimerStatus } from '../../actions';
|
||||
|
||||
import Dashboard from '../../containers/Dashboard';
|
||||
@ -35,15 +33,19 @@ import SetupGuide from '../../containers/SetupGuide';
|
||||
import Settings from '../../containers/Settings';
|
||||
import Dns from '../../containers/Dns';
|
||||
import Encryption from '../../containers/Encryption';
|
||||
|
||||
import Dhcp from '../Settings/Dhcp';
|
||||
import Clients from '../../containers/Clients';
|
||||
import DnsBlocklist from '../../containers/DnsBlocklist';
|
||||
import DnsAllowlist from '../../containers/DnsAllowlist';
|
||||
import DnsRewrites from '../../containers/DnsRewrites';
|
||||
import CustomRules from '../../containers/CustomRules';
|
||||
|
||||
import Services from '../Filters/Services';
|
||||
|
||||
import Logs from '../Logs';
|
||||
import ProtectionTimer from '../ProtectionTimer';
|
||||
import { RootState } from '../../initialState';
|
||||
|
||||
const ROUTES = [
|
||||
{
|
||||
@ -101,26 +103,17 @@ const ROUTES = [
|
||||
},
|
||||
];
|
||||
|
||||
const renderRoute = ({ path, component, exact }, idx) => <Route
|
||||
key={idx}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>;
|
||||
|
||||
const App = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
language,
|
||||
isCoreRunning,
|
||||
isUpdateAvailable,
|
||||
processing,
|
||||
theme,
|
||||
} = useSelector((state) => state.dashboard, shallowEqual);
|
||||
const { language, isCoreRunning, isUpdateAvailable, processing, theme } = useSelector<
|
||||
RootState,
|
||||
RootState['dashboard']
|
||||
>((state) => state.dashboard, shallowEqual);
|
||||
|
||||
const { processing: processingEncryption } = useSelector((
|
||||
state,
|
||||
) => state.encryption, shallowEqual);
|
||||
const { processing: processingEncryption } = useSelector<RootState, RootState['encryption']>(
|
||||
(state) => state.encryption,
|
||||
shallowEqual,
|
||||
);
|
||||
|
||||
const updateAvailable = isCoreRunning && isUpdateAvailable;
|
||||
|
||||
@ -157,7 +150,7 @@ const App = () => {
|
||||
setLanguage();
|
||||
}, [language]);
|
||||
|
||||
const handleAutoTheme = (e, accountTheme) => {
|
||||
const handleAutoTheme = (e: any, accountTheme: any) => {
|
||||
if (accountTheme !== THEMES.auto) {
|
||||
return;
|
||||
}
|
||||
@ -195,35 +188,50 @@ const App = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return <HashRouter hashType="noslash">
|
||||
{updateAvailable && <>
|
||||
<UpdateTopline />
|
||||
<UpdateOverlay />
|
||||
</>}
|
||||
{!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>;
|
||||
};
|
||||
return (
|
||||
<HashRouter hashType="noslash">
|
||||
{updateAvailable && (
|
||||
<>
|
||||
<UpdateTopline />
|
||||
|
||||
renderRoute.propTypes = {
|
||||
path: propTypes.oneOfType([propTypes.string, propTypes.arrayOf(propTypes.string)]).isRequired,
|
||||
component: propTypes.element.isRequired,
|
||||
exact: propTypes.bool,
|
||||
<UpdateOverlay />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!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);
|
@ -1,25 +1,37 @@
|
||||
import React from 'react';
|
||||
|
||||
// @ts-expect-error FIXME: update react-table
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { TFunction } from 'i18next';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import Cell from '../ui/Cell';
|
||||
|
||||
import DomainCell from './DomainCell';
|
||||
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
|
||||
|
||||
const CountCell = (totalBlocked) => function cell(row) {
|
||||
const { value } = row;
|
||||
const percent = getPercent(totalBlocked, value);
|
||||
const CountCell = (totalBlocked: any) =>
|
||||
function cell(row: any) {
|
||||
const { value } = row;
|
||||
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} search={row.original.domain} />;
|
||||
};
|
||||
|
||||
interface BlockedDomainsProps {
|
||||
topBlockedDomains: unknown[];
|
||||
blockedFiltering: number;
|
||||
replacedSafebrowsing: number;
|
||||
replacedSafesearch: number;
|
||||
replacedParental: number;
|
||||
refreshButton: React.ReactNode;
|
||||
subtitle: string;
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
const BlockedDomains = ({
|
||||
t,
|
||||
@ -30,20 +42,13 @@ const BlockedDomains = ({
|
||||
replacedSafebrowsing,
|
||||
replacedParental,
|
||||
replacedSafesearch,
|
||||
}) => {
|
||||
const totalBlocked = (
|
||||
blockedFiltering + replacedSafebrowsing + replacedParental + replacedSafesearch
|
||||
);
|
||||
}: BlockedDomainsProps) => {
|
||||
const totalBlocked = blockedFiltering + replacedSafebrowsing + replacedParental + replacedSafesearch;
|
||||
|
||||
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
|
||||
data={topBlockedDomains.map(({ name: domain, count }) => ({
|
||||
data={topBlockedDomains.map(({ name: domain, count }: any) => ({
|
||||
domain,
|
||||
count,
|
||||
}))}
|
||||
@ -70,15 +75,4 @@ 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);
|
@ -1,10 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
// @ts-expect-error FIXME: update react-table
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
|
||||
@ -16,11 +18,14 @@ import {
|
||||
TABLES_MIN_ROWS,
|
||||
} from '../../helpers/constants';
|
||||
import { toggleClientBlock } from '../../actions/access';
|
||||
|
||||
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
|
||||
import { getStats } from '../../actions/stats';
|
||||
import IconTooltip from '../Logs/Cells/IconTooltip';
|
||||
|
||||
const getClientsPercentColor = (percent) => {
|
||||
import IconTooltip from '../Logs/Cells/IconTooltip';
|
||||
import { RootState } from '../../initialState';
|
||||
|
||||
const getClientsPercentColor = (percent: any) => {
|
||||
if (percent > 50) {
|
||||
return STATUS_COLORS.green;
|
||||
}
|
||||
@ -30,9 +35,13 @@ const getClientsPercentColor = (percent) => {
|
||||
return STATUS_COLORS.red;
|
||||
};
|
||||
|
||||
const CountCell = (row) => {
|
||||
const { value, original: { ip } } = row;
|
||||
const numDnsQueries = useSelector((state) => state.stats.numDnsQueries, shallowEqual);
|
||||
const CountCell = (row: any) => {
|
||||
const {
|
||||
value,
|
||||
original: { ip },
|
||||
} = row;
|
||||
|
||||
const numDnsQueries = useSelector<RootState>((state) => state.stats.numDnsQueries, shallowEqual);
|
||||
|
||||
const percent = getPercent(numDnsQueries, value);
|
||||
const percentColor = getClientsPercentColor(percent);
|
||||
@ -40,22 +49,29 @@ const CountCell = (row) => {
|
||||
return <Cell value={value} percent={percent} color={percentColor} search={ip} />;
|
||||
};
|
||||
|
||||
const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
|
||||
const renderBlockingButton = (ip: any, disallowed: any, disallowed_rule: any) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const processingSet = useSelector((state) => state.access.processingSet);
|
||||
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
|
||||
|
||||
const processingSet = useSelector<RootState, RootState['access']['processingSet']>(
|
||||
(state) => state.access.processingSet,
|
||||
);
|
||||
|
||||
const allowedClients = useSelector<RootState, RootState['access']['allowed_clients']>(
|
||||
(state) => state.access.allowed_clients,
|
||||
shallowEqual,
|
||||
);
|
||||
|
||||
const [isOptionsOpened, setOptionsOpened] = useState(false);
|
||||
|
||||
const toggleClientStatus = async (ip, disallowed, disallowed_rule) => {
|
||||
const toggleClientStatus = async (ip: any, disallowed: any, disallowed_rule: any) => {
|
||||
let confirmMessage;
|
||||
|
||||
if (disallowed) {
|
||||
confirmMessage = t('client_confirm_unblock', { ip: disallowed_rule || ip });
|
||||
} else {
|
||||
confirmMessage = `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`;
|
||||
if (allowedСlients.length > 0) {
|
||||
if (allowedClients.length > 0) {
|
||||
confirmMessage = confirmMessage.concat(`\n\n${t('filter_allowlist', { disallowed_rule })}`);
|
||||
}
|
||||
}
|
||||
@ -73,15 +89,11 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
|
||||
|
||||
const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
|
||||
const lastRuleInAllowlist = !disallowed && allowedСlients === disallowed_rule;
|
||||
const lastRuleInAllowlist = !disallowed && allowedClients === disallowed_rule;
|
||||
const disabled = processingSet || lastRuleInAllowlist;
|
||||
return (
|
||||
<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">
|
||||
<use xlinkHref="#bullets" />
|
||||
</svg>
|
||||
@ -92,16 +104,18 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
|
||||
tooltipClass="button-action--arrow-option-container"
|
||||
xlinkHref="bullets"
|
||||
triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
|
||||
content={(
|
||||
content={
|
||||
<button
|
||||
className={classNames('button-action--arrow-option px-4 py-1', disallowed ? 'bg--green' : 'bg--danger')}
|
||||
className={classNames(
|
||||
'button-action--arrow-option px-4 py-1',
|
||||
disallowed ? 'bg--green' : 'bg--danger',
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}
|
||||
>
|
||||
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}>
|
||||
<Trans>{text}</Trans>
|
||||
</button>
|
||||
)}
|
||||
}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
onVisibilityChange={setOptionsOpened}
|
||||
@ -113,35 +127,42 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ClientCell = (row) => {
|
||||
const { value, original: { info, info: { disallowed, disallowed_rule } } } = row;
|
||||
|
||||
return <>
|
||||
<div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
|
||||
{renderFormattedClientCell(value, info, true)}
|
||||
{renderBlockingButton(value, disallowed, disallowed_rule)}
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
const Clients = ({
|
||||
refreshButton,
|
||||
subtitle,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
|
||||
const ClientCell = (row: any) => {
|
||||
const {
|
||||
value,
|
||||
original: {
|
||||
info,
|
||||
info: { disallowed, disallowed_rule },
|
||||
},
|
||||
} = row;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('top_clients')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
<>
|
||||
<div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
|
||||
{renderFormattedClientCell(value, info, true)}
|
||||
{renderBlockingButton(value, disallowed, disallowed_rule)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ClientsProps {
|
||||
refreshButton: React.ReactNode;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
const Clients = ({ refreshButton, subtitle }: ClientsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const topClients = useSelector<RootState, RootState['stats']['topClients']>(
|
||||
(state) => state.stats.topClients,
|
||||
shallowEqual,
|
||||
);
|
||||
|
||||
return (
|
||||
<Card title={t('top_clients')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
|
||||
<ReactTable
|
||||
data={topClients.map(({
|
||||
name: ip, count, info, blocked,
|
||||
}) => ({
|
||||
data={topClients.map(({ name: ip, count, info, blocked }: any) => ({
|
||||
ip,
|
||||
count,
|
||||
info,
|
||||
@ -167,12 +188,14 @@ const Clients = ({
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
|
||||
className="-highlight card-table-overflow--limited clients__table"
|
||||
getTrProps={(_state, rowInfo) => {
|
||||
getTrProps={(_state: any, rowInfo: any) => {
|
||||
if (!rowInfo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { info: { disallowed } } = rowInfo.original;
|
||||
const {
|
||||
info: { disallowed },
|
||||
} = rowInfo.original;
|
||||
|
||||
return disallowed ? { className: 'logs__row--red' } : {};
|
||||
}}
|
||||
@ -181,9 +204,4 @@ const Clients = ({
|
||||
);
|
||||
};
|
||||
|
||||
Clients.propTypes = {
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Clients;
|
@ -1,41 +1,52 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import round from 'lodash/round';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import { formatNumber, msToDays, msToHours } from '../../helpers/helpers';
|
||||
|
||||
import LogsSearchLink from '../ui/LogsSearchLink';
|
||||
import { RESPONSE_FILTER, TIME_UNITS } from '../../helpers/constants';
|
||||
import Tooltip from '../ui/Tooltip';
|
||||
|
||||
const Row = ({
|
||||
label, count, response_status, tooltipTitle, translationComponents,
|
||||
}) => {
|
||||
const content = response_status
|
||||
? <LogsSearchLink response_status={response_status}>{formatNumber(count)}</LogsSearchLink>
|
||||
: count;
|
||||
import Tooltip from '../ui/Tooltip';
|
||||
import { RootState } from '../../initialState';
|
||||
|
||||
interface RowProps {
|
||||
label: string;
|
||||
count: string;
|
||||
response_status?: string;
|
||||
tooltipTitle: string;
|
||||
translationComponents?: React.ReactElement[];
|
||||
}
|
||||
|
||||
const Row = ({ label, count, response_status, tooltipTitle, translationComponents }: RowProps) => {
|
||||
const content = response_status ? (
|
||||
<LogsSearchLink response_status={response_status}>{formatNumber(count)}</LogsSearchLink>
|
||||
) : (
|
||||
count
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="counters__row" key={label}>
|
||||
<div className="counters__column">
|
||||
<span className="counters__title">
|
||||
<Trans components={translationComponents}>
|
||||
{label}
|
||||
</Trans>
|
||||
<Trans components={translationComponents}>{label}</Trans>
|
||||
</span>
|
||||
|
||||
<span className="counters__tooltip">
|
||||
<Tooltip
|
||||
content={tooltipTitle}
|
||||
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">
|
||||
<use xlinkHref="#question" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="counters__column counters__column--value">
|
||||
<strong>{content}</strong>
|
||||
</div>
|
||||
@ -43,7 +54,12 @@ const Row = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Counters = ({ refreshButton, subtitle }) => {
|
||||
interface CountersProps {
|
||||
refreshButton: React.ReactNode;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
const Counters = ({ refreshButton, subtitle }: CountersProps) => {
|
||||
const {
|
||||
interval,
|
||||
numDnsQueries,
|
||||
@ -53,77 +69,67 @@ const Counters = ({ refreshButton, subtitle }) => {
|
||||
numReplacedSafesearch,
|
||||
avgProcessingTime,
|
||||
timeUnits,
|
||||
} = useSelector((state) => state.stats, shallowEqual);
|
||||
} = useSelector<RootState, RootState['stats']>((state) => state.stats, shallowEqual);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dnsQueryTooltip = timeUnits === TIME_UNITS.HOURS
|
||||
? t('number_of_dns_query_hours', { count: msToHours(interval) })
|
||||
: t('number_of_dns_query_days', { count: msToDays(interval) });
|
||||
const dnsQueryTooltip =
|
||||
timeUnits === TIME_UNITS.HOURS
|
||||
? t('number_of_dns_query_hours', { count: msToHours(interval) })
|
||||
: t('number_of_dns_query_days', { count: msToDays(interval) });
|
||||
|
||||
const rows = [
|
||||
{
|
||||
label: 'dns_query',
|
||||
count: numDnsQueries,
|
||||
count: numDnsQueries.toString(),
|
||||
tooltipTitle: dnsQueryTooltip,
|
||||
response_status: RESPONSE_FILTER.ALL.QUERY,
|
||||
},
|
||||
{
|
||||
label: 'blocked_by',
|
||||
count: numBlockedFiltering,
|
||||
count: numBlockedFiltering.toString(),
|
||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours',
|
||||
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',
|
||||
count: numReplacedSafebrowsing,
|
||||
count: numReplacedSafebrowsing.toString(),
|
||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec',
|
||||
response_status: RESPONSE_FILTER.BLOCKED_THREATS.QUERY,
|
||||
},
|
||||
{
|
||||
label: 'stats_adult',
|
||||
count: numReplacedParental,
|
||||
count: numReplacedParental.toString(),
|
||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult',
|
||||
response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY,
|
||||
},
|
||||
{
|
||||
label: 'enforced_save_search',
|
||||
count: numReplacedSafesearch,
|
||||
count: numReplacedSafesearch.toString(),
|
||||
tooltipTitle: 'number_of_dns_query_to_safe_search',
|
||||
response_status: RESPONSE_FILTER.SAFE_SEARCH.QUERY,
|
||||
},
|
||||
{
|
||||
label: 'average_processing_time',
|
||||
count: avgProcessingTime ? `${round(avgProcessingTime)} ms` : 0,
|
||||
count: avgProcessingTime ? `${round(avgProcessingTime)} ms` : '0',
|
||||
tooltipTitle: 'average_processing_time_hint',
|
||||
},
|
||||
];
|
||||
|
||||
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">
|
||||
{rows.map(Row)}
|
||||
{rows.map((row, index) => {
|
||||
return <Row {...row} key={index} />;
|
||||
})}
|
||||
</div>
|
||||
</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;
|
@ -1,77 +0,0 @@
|
||||
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;
|
81
client/src/components/Dashboard/DomainCell.tsx
Normal file
81
client/src/components/Dashboard/DomainCell.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
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;
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
// @ts-expect-error FIXME: update react-table
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
@ -8,9 +9,10 @@ import Cell from '../ui/Cell';
|
||||
import DomainCell from './DomainCell';
|
||||
|
||||
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
|
||||
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
|
||||
const getQueriedPercentColor = (percent) => {
|
||||
const getQueriedPercentColor = (percent: any) => {
|
||||
if (percent > 10) {
|
||||
return STATUS_COLORS.red;
|
||||
}
|
||||
@ -20,26 +22,27 @@ const getQueriedPercentColor = (percent) => {
|
||||
return STATUS_COLORS.green;
|
||||
};
|
||||
|
||||
const countCell = (dnsQueries) => function cell(row) {
|
||||
const { value } = row;
|
||||
const percent = getPercent(dnsQueries, value);
|
||||
const percentColor = getQueriedPercentColor(percent);
|
||||
const countCell = (dnsQueries: any) =>
|
||||
function cell(row: any) {
|
||||
const { value } = row;
|
||||
const percent = getPercent(dnsQueries, value);
|
||||
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} />;
|
||||
};
|
||||
|
||||
const QueriedDomains = ({
|
||||
t, refreshButton, topQueriedDomains, subtitle, dnsQueries,
|
||||
}) => (
|
||||
<Card
|
||||
title={t('stats_query_domain')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
interface QueriedDomainsProps {
|
||||
topQueriedDomains: unknown[];
|
||||
dnsQueries: number;
|
||||
refreshButton: React.ReactNode;
|
||||
subtitle: string;
|
||||
t: (...args: unknown[]) => string;
|
||||
}
|
||||
|
||||
const QueriedDomains = ({ t, refreshButton, topQueriedDomains, subtitle, dnsQueries }: QueriedDomainsProps) => (
|
||||
<Card title={t('stats_query_domain')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
|
||||
<ReactTable
|
||||
data={topQueriedDomains.map(({ name: domain, count }) => ({
|
||||
data={topQueriedDomains.map(({ name: domain, count }: any) => ({
|
||||
domain,
|
||||
count,
|
||||
}))}
|
||||
@ -65,12 +68,4 @@ const QueriedDomains = ({
|
||||
</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);
|
@ -1,15 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import StatsCard from './StatsCard';
|
||||
|
||||
import { getPercent, normalizeHistory } from '../../helpers/helpers';
|
||||
import { RESPONSE_FILTER } from '../../helpers/constants';
|
||||
|
||||
const getNormalizedHistory = (data, interval, id) => [
|
||||
{ data: normalizeHistory(data, interval), id },
|
||||
];
|
||||
const getNormalizedHistory = (data: any, interval: any, id: any) => [{ data: normalizeHistory(data), 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 = ({
|
||||
interval,
|
||||
@ -21,61 +33,68 @@ const Statistics = ({
|
||||
numBlockedFiltering,
|
||||
numReplacedSafebrowsing,
|
||||
numReplacedParental,
|
||||
}) => (
|
||||
}: StatisticsProps) => (
|
||||
<div className="row">
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<StatsCard
|
||||
total={numDnsQueries}
|
||||
lineData={getNormalizedHistory(dnsQueries, interval, 'dnsQuery')}
|
||||
title={<Link to="logs"><Trans>dns_query</Trans></Link>}
|
||||
title={
|
||||
<Link to="logs">
|
||||
<Trans>dns_query</Trans>
|
||||
</Link>
|
||||
}
|
||||
color="blue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<StatsCard
|
||||
total={numBlockedFiltering}
|
||||
lineData={getNormalizedHistory(blockedFiltering, interval, 'blockedFiltering')}
|
||||
percent={getPercent(numDnsQueries, numBlockedFiltering)}
|
||||
title={<Trans components={[<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED.QUERY}`} key="0">link</Link>]}>blocked_by</Trans>}
|
||||
title={
|
||||
<Trans
|
||||
components={[
|
||||
<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED.QUERY}`} key="0">
|
||||
link
|
||||
</Link>,
|
||||
]}>
|
||||
blocked_by
|
||||
</Trans>
|
||||
}
|
||||
color="red"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<StatsCard
|
||||
total={numReplacedSafebrowsing}
|
||||
lineData={getNormalizedHistory(
|
||||
replacedSafebrowsing,
|
||||
interval,
|
||||
'replacedSafebrowsing',
|
||||
)}
|
||||
lineData={getNormalizedHistory(replacedSafebrowsing, interval, 'replacedSafebrowsing')}
|
||||
percent={getPercent(numDnsQueries, numReplacedSafebrowsing)}
|
||||
title={<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_THREATS.QUERY}`}><Trans>stats_malware_phishing</Trans></Link>}
|
||||
title={
|
||||
<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_THREATS.QUERY}`}>
|
||||
<Trans>stats_malware_phishing</Trans>
|
||||
</Link>
|
||||
}
|
||||
color="green"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<StatsCard
|
||||
total={numReplacedParental}
|
||||
lineData={getNormalizedHistory(replacedParental, interval, 'replacedParental')}
|
||||
percent={getPercent(numDnsQueries, numReplacedParental)}
|
||||
title={<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY}`}><Trans>stats_adult</Trans></Link>}
|
||||
title={
|
||||
<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY}`}>
|
||||
<Trans>stats_adult</Trans>
|
||||
</Link>
|
||||
}
|
||||
color="yellow"
|
||||
/>
|
||||
</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);
|
@ -1,38 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
|
||||
import { formatNumber } from '../../helpers/helpers';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import Line from '../ui/Line';
|
||||
|
||||
const StatsCard = ({
|
||||
total, lineData, percent, title, color,
|
||||
}) => (
|
||||
interface StatsCardProps {
|
||||
total: number;
|
||||
lineData: unknown[];
|
||||
title: object;
|
||||
color: string;
|
||||
percent?: number;
|
||||
}
|
||||
|
||||
const StatsCard = ({ total, lineData, percent, title, color }: StatsCardProps) => (
|
||||
<Card type="card--full" bodyType="card-wrap">
|
||||
<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>
|
||||
{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">
|
||||
<Line data={lineData} color={STATUS_COLORS[color]} />
|
||||
</div>
|
||||
</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;
|
@ -1,50 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
// @ts-expect-error FIXME: update react-table
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import round from 'lodash/round';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { TFunction } from 'i18next';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import DomainCell from './DomainCell';
|
||||
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, TABLES_MIN_ROWS } from '../../helpers/constants';
|
||||
|
||||
const TimeCell = ({ value }) => {
|
||||
interface TimeCellProps {
|
||||
value?: string | number;
|
||||
}
|
||||
|
||||
const TimeCell = ({ value }: TimeCellProps) => {
|
||||
if (!value) {
|
||||
return '–';
|
||||
}
|
||||
|
||||
const valueInMilliseconds = round(value * 1000);
|
||||
const valueInMilliseconds = round(Number(value) * 1000);
|
||||
|
||||
return (
|
||||
<div className="logs__row o-hidden">
|
||||
<span className="logs__text logs__text--full" title={valueInMilliseconds}>
|
||||
<span className="logs__text logs__text--full" title={valueInMilliseconds.toString()}>
|
||||
{valueInMilliseconds} ms
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimeCell.propTypes = {
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
};
|
||||
interface UpstreamAvgTimeProps {
|
||||
topUpstreamsAvgTime: { name: string; count: number }[];
|
||||
refreshButton: React.ReactNode;
|
||||
subtitle: string;
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
const UpstreamAvgTime = ({
|
||||
t,
|
||||
refreshButton,
|
||||
topUpstreamsAvgTime,
|
||||
subtitle,
|
||||
}) => (
|
||||
<Card
|
||||
title={t('average_upstream_response_time')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
const UpstreamAvgTime = ({ t, refreshButton, topUpstreamsAvgTime, subtitle }: UpstreamAvgTimeProps) => (
|
||||
<Card title={t('average_upstream_response_time')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
|
||||
<ReactTable
|
||||
data={topUpstreamsAvgTime.map(({ name: domain, count }) => ({
|
||||
data={topUpstreamsAvgTime.map(({ name: domain, count }: { name: string; count: number }) => ({
|
||||
domain,
|
||||
count,
|
||||
}))}
|
||||
@ -70,11 +67,4 @@ const UpstreamAvgTime = ({
|
||||
</Card>
|
||||
);
|
||||
|
||||
UpstreamAvgTime.propTypes = {
|
||||
topUpstreamsAvgTime: PropTypes.array.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(UpstreamAvgTime);
|
@ -1,51 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
// @ts-expect-error FIXME: update react-table
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import { TFunction } from 'i18next';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import Cell from '../ui/Cell';
|
||||
|
||||
import DomainCell from './DomainCell';
|
||||
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
|
||||
|
||||
const CountCell = (totalBlocked) => (
|
||||
function cell(row) {
|
||||
const CountCell = (totalBlocked: any) =>
|
||||
function cell(row: any) {
|
||||
const { value } = row;
|
||||
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) => {
|
||||
const getTotalUpstreamRequests = (stats: any) => {
|
||||
let total = 0;
|
||||
stats.forEach(({ count }) => { total += count; });
|
||||
stats.forEach(({ count }: any) => {
|
||||
total += count;
|
||||
});
|
||||
|
||||
return total;
|
||||
};
|
||||
|
||||
const UpstreamResponses = ({
|
||||
t,
|
||||
refreshButton,
|
||||
topUpstreamsResponses,
|
||||
subtitle,
|
||||
}) => (
|
||||
<Card
|
||||
title={t('top_upstreams')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
interface UpstreamResponsesProps {
|
||||
topUpstreamsResponses: { name: string; count: number }[];
|
||||
refreshButton: React.ReactNode;
|
||||
subtitle: string;
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
const UpstreamResponses = ({ t, refreshButton, topUpstreamsResponses, subtitle }: UpstreamResponsesProps) => (
|
||||
<Card title={t('top_upstreams')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
|
||||
<ReactTable
|
||||
data={topUpstreamsResponses.map(({ name: domain, count }) => ({
|
||||
data={topUpstreamsResponses.map(({ name: domain, count }: { name: string; count: number }) => ({
|
||||
domain,
|
||||
count,
|
||||
}))}
|
||||
@ -71,11 +67,4 @@ const UpstreamResponses = ({
|
||||
</Card>
|
||||
);
|
||||
|
||||
UpstreamResponses.propTypes = {
|
||||
topUpstreamsResponses: PropTypes.array.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(UpstreamResponses);
|
@ -1,276 +0,0 @@
|
||||
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;
|
260
client/src/components/Dashboard/index.tsx
Normal file
260
client/src/components/Dashboard/index.tsx
Normal file
@ -0,0 +1,260 @@
|
||||
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;
|
@ -1,32 +0,0 @@
|
||||
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);
|
27
client/src/components/Filters/Actions.tsx
Normal file
27
client/src/components/Filters/Actions.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
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);
|
@ -15,10 +15,12 @@ import {
|
||||
getRulesToFilterList,
|
||||
} from '../../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants';
|
||||
import { toggleBlocking } from '../../../actions';
|
||||
|
||||
const renderBlockingButton = (isFiltered, domain) => {
|
||||
const processingRules = useSelector((state) => state.filtering.processingRules);
|
||||
import { toggleBlocking } from '../../../actions';
|
||||
import { RootState } from '../../../initialState';
|
||||
|
||||
const renderBlockingButton = (isFiltered: any, domain: any) => {
|
||||
const processingRules = useSelector((state: RootState) => state.filtering.processingRules);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -28,28 +30,32 @@ const renderBlockingButton = (isFiltered, domain) => {
|
||||
await dispatch(toggleBlocking(buttonType, domain));
|
||||
};
|
||||
|
||||
const buttonClass = classNames('mt-3 button-action button-action--main button-action--active button-action--small', {
|
||||
'button-action--unblock': isFiltered,
|
||||
});
|
||||
const buttonClass = classNames(
|
||||
'mt-3 button-action button-action--main button-action--active button-action--small',
|
||||
{
|
||||
'button-action--unblock': isFiltered,
|
||||
},
|
||||
);
|
||||
|
||||
return <button type="button"
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={processingRules}
|
||||
>
|
||||
return (
|
||||
<button type="button" className={buttonClass} onClick={onClick} disabled={processingRules}>
|
||||
{t(buttonType)}
|
||||
</button>;
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
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 filters = useSelector((state: RootState) => state.filtering.filters, shallowEqual);
|
||||
|
||||
const getReasonFiltered = (reason) => {
|
||||
const whitelistFilters = useSelector((state: RootState) => state.filtering.whitelistFilters, shallowEqual);
|
||||
|
||||
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, '');
|
||||
return i18next.t('query_log_filtered', { filter: filterKey });
|
||||
};
|
||||
@ -71,24 +77,23 @@ const getTitle = () => {
|
||||
return REASON_TO_TITLE_MAP[reason];
|
||||
}
|
||||
|
||||
return <>
|
||||
<div>{t('check_reason', { reason })}</div>
|
||||
<div>
|
||||
{t('rule_label')}:
|
||||
|
||||
{ruleAndFilterNames}
|
||||
</div>
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<div>{t('check_reason', { reason })}</div>
|
||||
|
||||
<div>
|
||||
{t('rule_label')}:
|
||||
{ruleAndFilterNames}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Info = () => {
|
||||
const {
|
||||
hostname,
|
||||
reason,
|
||||
service_name,
|
||||
cname,
|
||||
ip_addrs,
|
||||
} = useSelector((state) => state.filtering.check, shallowEqual);
|
||||
const { hostname, reason, service_name, cname, ip_addrs } = useSelector(
|
||||
(state: RootState) => state.filtering.check,
|
||||
shallowEqual,
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const title = getTitle();
|
||||
@ -99,23 +104,29 @@ const Info = () => {
|
||||
'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);
|
||||
|
||||
return <div className={className}>
|
||||
<div><strong>{hostname}</strong></div>
|
||||
<div>{title}</div>
|
||||
{!onlyFiltered
|
||||
&& <>
|
||||
{service_name && <div>{t('check_service', { service: service_name })}</div>}
|
||||
{cname && <div>{t('check_cname', { cname })}</div>}
|
||||
{ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>}
|
||||
{renderBlockingButton(isFiltered, hostname)}
|
||||
</>}
|
||||
</div>;
|
||||
return (
|
||||
<div className={className}>
|
||||
<div>
|
||||
<strong>{hostname}</strong>
|
||||
</div>
|
||||
|
||||
<div>{title}</div>
|
||||
{!onlyFiltered && (
|
||||
<>
|
||||
{service_name && <div>{t('check_service', { service: service_name })}</div>}
|
||||
|
||||
{cname && <div>{t('check_cname', { cname })}</div>}
|
||||
|
||||
{ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>}
|
||||
{renderBlockingButton(isFiltered, hostname)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
@ -1,66 +0,0 @@
|
||||
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);
|
70
client/src/components/Filters/Check/index.tsx
Normal file
70
client/src/components/Filters/Check/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
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);
|
@ -1,32 +1,46 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
|
||||
import Examples from './Examples';
|
||||
|
||||
import Check from './Check';
|
||||
|
||||
import { getTextareaCommentsHighlight, syncScroll } from '../../helpers/highlightTextareaComments';
|
||||
import { COMMENT_LINE_DEFAULT_TOKEN } from '../../helpers/constants';
|
||||
import '../ui/texareaCommentsHighlight.css';
|
||||
import { FilteringData } from '../../initialState';
|
||||
|
||||
class CustomRules extends Component {
|
||||
interface CustomRulesProps {
|
||||
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();
|
||||
|
||||
componentDidMount() {
|
||||
this.props.getFilteringStatus();
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
handleChange = (e: any) => {
|
||||
const { value } = e.currentTarget;
|
||||
this.handleRulesChange(value);
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
handleSubmit = (e: any) => {
|
||||
e.preventDefault();
|
||||
this.handleRulesSubmit();
|
||||
};
|
||||
|
||||
handleRulesChange = (value) => {
|
||||
handleRulesChange = (value: any) => {
|
||||
this.props.handleRulesChange({ userRules: value });
|
||||
};
|
||||
|
||||
@ -34,23 +48,22 @@ class CustomRules extends Component {
|
||||
this.props.setRules(this.props.filtering.userRules);
|
||||
};
|
||||
|
||||
handleCheck = (values) => {
|
||||
handleCheck = (values: any) => {
|
||||
this.props.checkHost(values);
|
||||
};
|
||||
|
||||
onScroll = (e) => syncScroll(e, this.ref)
|
||||
onScroll = (e: any) => syncScroll(e, this.ref);
|
||||
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
filtering: {
|
||||
userRules,
|
||||
},
|
||||
filtering: { userRules },
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={t('custom_filtering_rules')} />
|
||||
|
||||
<Card subtitle={t('custom_filter_rules_hint')}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="text-edit-container mb-4">
|
||||
@ -60,39 +73,31 @@ class CustomRules extends Component {
|
||||
onChange={this.handleChange}
|
||||
onScroll={this.onScroll}
|
||||
/>
|
||||
{getTextareaCommentsHighlight(
|
||||
this.ref,
|
||||
userRules,
|
||||
undefined,
|
||||
[COMMENT_LINE_DEFAULT_TOKEN, '!'],
|
||||
)}
|
||||
{getTextareaCommentsHighlight(this.ref, userRules, [
|
||||
COMMENT_LINE_DEFAULT_TOKEN,
|
||||
'!',
|
||||
])}
|
||||
</div>
|
||||
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
onClick={this.handleSubmit}>
|
||||
<Trans>apply_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
|
||||
<Examples />
|
||||
</Card>
|
||||
|
||||
<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);
|
@ -1,5 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
@ -9,15 +8,41 @@ import Actions from './Actions';
|
||||
import Table from './Table';
|
||||
|
||||
import { MODAL_TYPE } from '../../helpers/constants';
|
||||
|
||||
import { getCurrentFilter } from '../../helpers/helpers';
|
||||
|
||||
class DnsAllowlist extends Component {
|
||||
interface DnsAllowlistProps {
|
||||
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() {
|
||||
this.props.getFilteringStatus();
|
||||
}
|
||||
|
||||
handleSubmit = (values) => {
|
||||
handleSubmit = (values: any) => {
|
||||
const { name, url } = values;
|
||||
|
||||
const { filtering } = this.props;
|
||||
const whitelist = true;
|
||||
|
||||
@ -28,15 +53,17 @@ class DnsAllowlist extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleDelete = (url) => {
|
||||
handleDelete = (url: any) => {
|
||||
if (window.confirm(this.props.t('list_confirm_delete'))) {
|
||||
const whitelist = true;
|
||||
|
||||
this.props.removeFilter(url, whitelist);
|
||||
}
|
||||
};
|
||||
|
||||
toggleFilter = (url, data) => {
|
||||
toggleFilter = (url: any, data: any) => {
|
||||
const whitelist = true;
|
||||
|
||||
this.props.toggleFilterStatus(url, data, whitelist);
|
||||
};
|
||||
|
||||
@ -53,7 +80,6 @@ class DnsAllowlist extends Component {
|
||||
t,
|
||||
toggleFilteringModal,
|
||||
addFilter,
|
||||
toggleFilterStatus,
|
||||
filtering: {
|
||||
whitelistFilters,
|
||||
isModalOpen,
|
||||
@ -68,19 +94,18 @@ class DnsAllowlist extends Component {
|
||||
},
|
||||
} = this.props;
|
||||
const currentFilterData = getCurrentFilter(modalFilterUrl, whitelistFilters);
|
||||
const loading = processingConfigFilter
|
||||
|| processingFilters
|
||||
|| processingAddFilter
|
||||
|| processingRemoveFilter
|
||||
|| processingRefreshFilters;
|
||||
const loading =
|
||||
processingConfigFilter ||
|
||||
processingFilters ||
|
||||
processingAddFilter ||
|
||||
processingRemoveFilter ||
|
||||
processingRefreshFilters;
|
||||
|
||||
const whitelist = true;
|
||||
|
||||
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="row">
|
||||
<div className="col-md-12">
|
||||
@ -90,11 +115,11 @@ class DnsAllowlist extends Component {
|
||||
loading={loading}
|
||||
processingConfigFilter={processingConfigFilter}
|
||||
toggleFilteringModal={toggleFilteringModal}
|
||||
toggleFilterStatus={toggleFilterStatus}
|
||||
handleDelete={this.handleDelete}
|
||||
toggleFilter={this.toggleFilter}
|
||||
whitelist={whitelist}
|
||||
/>
|
||||
|
||||
<Actions
|
||||
handleAdd={this.openAddFiltersModal}
|
||||
handleRefresh={this.handleRefresh}
|
||||
@ -105,6 +130,7 @@ class DnsAllowlist extends Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
filters={whitelistFilters}
|
||||
isOpen={isModalOpen}
|
||||
@ -123,17 +149,4 @@ class DnsAllowlist extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
@ -1,27 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import Modal from './Modal';
|
||||
import Actions from './Actions';
|
||||
|
||||
import Table from './Table';
|
||||
import { MODAL_TYPE } from '../../helpers/constants';
|
||||
|
||||
import {
|
||||
getCurrentFilter,
|
||||
} from '../../helpers/helpers';
|
||||
import { getCurrentFilter } from '../../helpers/helpers';
|
||||
|
||||
import filtersCatalog from '../../helpers/filters/filters';
|
||||
import { FilteringData } from '../../initialState';
|
||||
|
||||
class DnsBlocklist extends Component {
|
||||
interface DnsBlocklistProps {
|
||||
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() {
|
||||
this.props.getFilteringStatus();
|
||||
}
|
||||
|
||||
handleSubmit = (values) => {
|
||||
handleSubmit = (values: any) => {
|
||||
const { modalFilterUrl, modalType } = this.props.filtering;
|
||||
|
||||
switch (modalType) {
|
||||
@ -30,23 +41,25 @@ class DnsBlocklist extends Component {
|
||||
break;
|
||||
case MODAL_TYPE.ADD_FILTERS: {
|
||||
const { name, url } = values;
|
||||
|
||||
this.props.addFilter(url, name);
|
||||
break;
|
||||
}
|
||||
case MODAL_TYPE.CHOOSE_FILTERING_LIST: {
|
||||
const changedValues = Object.entries(values)?.reduce((acc, [key, value]) => {
|
||||
const changedValues = Object.entries(values)?.reduce((acc: any, [key, value]) => {
|
||||
if (value && key in filtersCatalog.filters) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
Object.keys(changedValues)
|
||||
.forEach((fieldName) => {
|
||||
// filterId is actually in the field name
|
||||
const { source, name } = filtersCatalog.filters[fieldName];
|
||||
this.props.addFilter(source, name);
|
||||
});
|
||||
Object.keys(changedValues).forEach((fieldName) => {
|
||||
// filterId is actually in the field name
|
||||
|
||||
const { source, name } = filtersCatalog.filters[fieldName];
|
||||
|
||||
this.props.addFilter(source, name);
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -54,13 +67,13 @@ class DnsBlocklist extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleDelete = (url) => {
|
||||
handleDelete = (url: any) => {
|
||||
if (window.confirm(this.props.t('list_confirm_delete'))) {
|
||||
this.props.removeFilter(url);
|
||||
}
|
||||
};
|
||||
|
||||
toggleFilter = (url, data) => {
|
||||
toggleFilter = (url: any, data: any) => {
|
||||
this.props.toggleFilterStatus(url, data);
|
||||
};
|
||||
|
||||
@ -75,8 +88,11 @@ class DnsBlocklist extends Component {
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
|
||||
toggleFilteringModal,
|
||||
|
||||
addFilter,
|
||||
|
||||
filtering: {
|
||||
filters,
|
||||
isModalOpen,
|
||||
@ -91,18 +107,17 @@ class DnsBlocklist extends Component {
|
||||
},
|
||||
} = this.props;
|
||||
const currentFilterData = getCurrentFilter(modalFilterUrl, filters);
|
||||
const loading = processingConfigFilter
|
||||
|| processingFilters
|
||||
|| processingAddFilter
|
||||
|| processingRemoveFilter
|
||||
|| processingRefreshFilters;
|
||||
const loading =
|
||||
processingConfigFilter ||
|
||||
processingFilters ||
|
||||
processingAddFilter ||
|
||||
processingRemoveFilter ||
|
||||
processingRefreshFilters;
|
||||
|
||||
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="row">
|
||||
<div className="col-md-12">
|
||||
@ -115,6 +130,7 @@ class DnsBlocklist extends Component {
|
||||
handleDelete={this.handleDelete}
|
||||
toggleFilter={this.toggleFilter}
|
||||
/>
|
||||
|
||||
<Actions
|
||||
handleAdd={this.openSelectTypeModal}
|
||||
handleRefresh={this.handleRefresh}
|
||||
@ -124,6 +140,7 @@ class DnsBlocklist extends Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
filtersCatalog={filtersCatalog}
|
||||
filters={filters}
|
||||
@ -142,17 +159,4 @@ class DnsBlocklist extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
@ -7,31 +7,37 @@ const Examples = () => (
|
||||
<Trans>examples_title</Trans>:
|
||||
<ol className="leading-loose">
|
||||
<li>
|
||||
<code>||example.org^</code>:
|
||||
<Trans>example_meaning_filter_block</Trans>
|
||||
<code>||example.org^</code>:<Trans>example_meaning_filter_block</Trans>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code> @@||example.org^</code>:
|
||||
<Trans>example_meaning_filter_whitelist</Trans>
|
||||
<code> @@||example.org^</code>:<Trans>example_meaning_filter_whitelist</Trans>
|
||||
</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>
|
||||
<code><Trans>example_comment</Trans></code>:
|
||||
<Trans>example_comment_meaning</Trans>
|
||||
<code>
|
||||
<Trans>example_comment</Trans>
|
||||
</code>
|
||||
:<Trans>example_comment_meaning</Trans>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code><Trans>example_comment_hash</Trans></code>:
|
||||
<Trans>example_comment_meaning</Trans>
|
||||
<code>
|
||||
<Trans>example_comment_hash</Trans>
|
||||
</code>
|
||||
:<Trans>example_comment_meaning</Trans>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code>/REGEX/</code>:
|
||||
<Trans>example_regex_meaning</Trans>
|
||||
<code>/REGEX/</code>:<Trans>example_regex_meaning</Trans>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<p className="mt-1">
|
||||
<Trans
|
||||
components={[
|
||||
@ -39,12 +45,10 @@ const Examples = () => (
|
||||
href="https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax&from=ui&app=home"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
key="0">
|
||||
link
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
]}>
|
||||
filtering_rules_learn_more
|
||||
</Trans>
|
||||
</p>
|
@ -1,191 +0,0 @@
|
||||
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);
|
208
client/src/components/Filters/Form.tsx
Normal file
208
client/src/components/Filters/Form.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
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);
|
@ -1,11 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ReactModal from 'react-modal';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { MODAL_TYPE } from '../../helpers/constants';
|
||||
|
||||
import Form from './Form';
|
||||
import '../ui/Modal.css';
|
||||
|
||||
import { getMap } from '../../helpers/helpers';
|
||||
|
||||
ReactModal.setAppElement('#root');
|
||||
@ -25,7 +27,7 @@ const MODAL_TYPE_TO_TITLE_TYPE_MAP = {
|
||||
* @returns {'new_allowlist' | 'edit_allowlist' | 'choose_allowlist' |
|
||||
* 'new_blocklist' | 'edit_blocklist' | 'choose_blocklist' | null}
|
||||
*/
|
||||
const getTitle = (modalType, whitelist) => {
|
||||
const getTitle = (modalType: any, whitelist: any) => {
|
||||
const titleType = MODAL_TYPE_TO_TITLE_TYPE_MAP[modalType];
|
||||
if (!titleType) {
|
||||
return null;
|
||||
@ -33,19 +35,39 @@ const getTitle = (modalType, whitelist) => {
|
||||
return `${titleType}_${whitelist ? 'allowlist' : 'blocklist'}`;
|
||||
};
|
||||
|
||||
const getSelectedValues = (filters, catalogSourcesToIdMap) => filters.reduce((acc, { url }) => {
|
||||
if (Object.prototype.hasOwnProperty.call(catalogSourcesToIdMap, url)) {
|
||||
const fieldId = `filter${catalogSourcesToIdMap[url]}`;
|
||||
acc.selectedFilterIds[fieldId] = true;
|
||||
acc.selectedSources[url] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {
|
||||
selectedFilterIds: {},
|
||||
selectedSources: {},
|
||||
});
|
||||
const getSelectedValues = (filters: any, catalogSourcesToIdMap: any) =>
|
||||
filters.reduce(
|
||||
(acc: any, { url }: any) => {
|
||||
if (Object.prototype.hasOwnProperty.call(catalogSourcesToIdMap, url)) {
|
||||
const fieldId = `filter${catalogSourcesToIdMap[url]}`;
|
||||
acc.selectedFilterIds[fieldId] = true;
|
||||
acc.selectedSources[url] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
selectedFilterIds: {},
|
||||
selectedSources: {},
|
||||
},
|
||||
);
|
||||
|
||||
class Modal extends Component {
|
||||
interface ModalProps {
|
||||
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 = () => {
|
||||
this.props.toggleFilteringModal();
|
||||
};
|
||||
@ -53,15 +75,25 @@ class Modal extends Component {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
|
||||
processingAddFilter,
|
||||
|
||||
processingConfigFilter,
|
||||
|
||||
handleSubmit,
|
||||
|
||||
modalType,
|
||||
|
||||
currentFilterData,
|
||||
|
||||
whitelist,
|
||||
|
||||
toggleFilteringModal,
|
||||
|
||||
filters,
|
||||
|
||||
t,
|
||||
|
||||
filtersCatalog,
|
||||
} = this.props;
|
||||
|
||||
@ -90,15 +122,16 @@ class Modal extends Component {
|
||||
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
|
||||
closeTimeoutMS={0}
|
||||
isOpen={isOpen}
|
||||
onRequestClose={this.closeModal}
|
||||
>
|
||||
onRequestClose={this.closeModal}>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
{title && <h4 className="modal-title">{title}</h4>}
|
||||
|
||||
<button type="button" className="close" onClick={this.closeModal}>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
selectedSources={selectedSources}
|
||||
initialValues={initialValues}
|
||||
@ -116,20 +149,4 @@ class Modal extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
@ -1,22 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import { renderInputField } from '../../../helpers/form';
|
||||
import { validateAnswer, validateDomain, validateRequiredValue } from '../../../helpers/validators';
|
||||
import { FORM_NAME } from '../../../helpers/constants';
|
||||
|
||||
const Form = (props) => {
|
||||
const {
|
||||
t,
|
||||
handleSubmit,
|
||||
reset,
|
||||
pristine,
|
||||
submitting,
|
||||
toggleRewritesModal,
|
||||
processingAdd,
|
||||
} = props;
|
||||
interface FormProps {
|
||||
pristine: boolean;
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
reset: (...args: unknown[]) => string;
|
||||
toggleRewritesModal: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
processingAdd: boolean;
|
||||
t: (...args: unknown[]) => string;
|
||||
initialValues?: object;
|
||||
}
|
||||
|
||||
const Form = (props: FormProps) => {
|
||||
const { t, handleSubmit, reset, pristine, submitting, toggleRewritesModal, processingAdd } = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
@ -35,22 +39,19 @@ const Form = (props) => {
|
||||
validate={[validateRequiredValue, validateDomain]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Trans>examples_title</Trans>:
|
||||
<ol className="leading-loose">
|
||||
<li>
|
||||
<code>example.org</code> – <Trans>example_rewrite_domain</Trans>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code>*.example.org</code> –
|
||||
<span>
|
||||
<Trans components={[<code key="0">text</code>]}>
|
||||
example_rewrite_wildcard
|
||||
</Trans>
|
||||
<Trans components={[<code key="0">text</code>]}>example_rewrite_wildcard</Trans>
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="answer"
|
||||
@ -63,14 +64,15 @@ const Form = (props) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ul>{['rewrite_ip_address',
|
||||
'rewrite_domain_name',
|
||||
'rewrite_A',
|
||||
'rewrite_AAAA']
|
||||
.map((str) => <li key={str}>
|
||||
<Trans components={[<code key="0">text</code>]}>{str}</Trans>
|
||||
</li>)
|
||||
}</ul>
|
||||
|
||||
<ul>
|
||||
{['rewrite_ip_address', 'rewrite_domain_name', 'rewrite_A', 'rewrite_AAAA'].map((str) => (
|
||||
<li key={str}>
|
||||
<Trans components={[<code key="0">text</code>]}>{str}</Trans>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="modal-footer">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
@ -80,15 +82,14 @@ const Form = (props) => {
|
||||
onClick={() => {
|
||||
reset();
|
||||
toggleRewritesModal();
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Trans>cancel_btn</Trans>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || pristine || processingAdd}
|
||||
>
|
||||
disabled={submitting || pristine || processingAdd}>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
@ -97,17 +98,6 @@ const Form = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
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([
|
||||
withTranslation(),
|
||||
reduxForm({
|
@ -1,12 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import { MODAL_TYPE } from '../../../helpers/constants';
|
||||
|
||||
import Form from './Form';
|
||||
|
||||
const Modal = (props) => {
|
||||
interface ModalProps {
|
||||
isModalOpen: boolean;
|
||||
handleSubmit: (values: any) => void;
|
||||
toggleRewritesModal: (...args: unknown[]) => unknown;
|
||||
processingAdd: boolean;
|
||||
processingDelete: boolean;
|
||||
modalType: string;
|
||||
currentRewrite?: object;
|
||||
}
|
||||
|
||||
const Modal = (props: ModalProps) => {
|
||||
const {
|
||||
isModalOpen,
|
||||
handleSubmit,
|
||||
@ -22,8 +33,7 @@ const Modal = (props) => {
|
||||
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
|
||||
closeTimeoutMS={0}
|
||||
isOpen={isModalOpen}
|
||||
onRequestClose={() => toggleRewritesModal()}
|
||||
>
|
||||
onRequestClose={() => toggleRewritesModal()}>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">
|
||||
@ -33,10 +43,12 @@ const Modal = (props) => {
|
||||
<Trans>rewrite_add</Trans>
|
||||
)}
|
||||
</h4>
|
||||
|
||||
<button type="button" className="close" onClick={() => toggleRewritesModal()}>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
initialValues={{ ...currentRewrite }}
|
||||
onSubmit={handleSubmit}
|
||||
@ -49,14 +61,4 @@ const Modal = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
@ -1,13 +1,26 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @ts-expect-error FIXME: update react-table
|
||||
import ReactTable from 'react-table';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { sortIp } from '../../../helpers/helpers';
|
||||
import { MODAL_TYPE, TABLES_MIN_ROWS } from '../../../helpers/constants';
|
||||
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
|
||||
|
||||
class Table extends Component {
|
||||
cellWrap = ({ value }) => (
|
||||
interface TableProps {
|
||||
t: (...args: unknown[]) => string;
|
||||
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">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
@ -33,7 +46,7 @@ class Table extends Component {
|
||||
maxWidth: 100,
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
Cell: (value) => {
|
||||
Cell: (value: any) => {
|
||||
const currentRewrite = {
|
||||
answer: value.row.answer,
|
||||
domain: value.row.domain,
|
||||
@ -51,8 +64,7 @@ class Table extends Component {
|
||||
});
|
||||
}}
|
||||
disabled={this.props.processingUpdate}
|
||||
title={this.props.t('edit_table_action')}
|
||||
>
|
||||
title={this.props.t('edit_table_action')}>
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#edit" />
|
||||
</svg>
|
||||
@ -62,8 +74,7 @@ class Table extends Component {
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||
onClick={() => this.props.handleDelete(currentRewrite)}
|
||||
title={this.props.t('delete_table_action')}
|
||||
>
|
||||
title={this.props.t('delete_table_action')}>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
@ -75,9 +86,7 @@ class Table extends Component {
|
||||
];
|
||||
|
||||
render() {
|
||||
const {
|
||||
t, list, processing, processingAdd, processingDelete,
|
||||
} = this.props;
|
||||
const { t, list, processing, processingAdd, processingDelete } = this.props;
|
||||
|
||||
return (
|
||||
<ReactTable
|
||||
@ -87,7 +96,9 @@ class Table extends Component {
|
||||
className="-striped -highlight card-table-overflow"
|
||||
showPagination
|
||||
defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE) || 10}
|
||||
onPageSizeChange={(size) => LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE, size)}
|
||||
onPageSizeChange={(size: any) =>
|
||||
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE, size)
|
||||
}
|
||||
minRows={TABLES_MIN_ROWS}
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
@ -101,15 +112,4 @@ class Table extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
@ -1,26 +1,39 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
|
||||
import Table from './Table';
|
||||
|
||||
import Modal from './Modal';
|
||||
|
||||
import Card from '../../ui/Card';
|
||||
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
import { MODAL_TYPE } from '../../../helpers/constants';
|
||||
import { RewritesData } from '../../../initialState';
|
||||
|
||||
class Rewrites extends Component {
|
||||
interface RewritesProps {
|
||||
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() {
|
||||
this.props.getRewritesList();
|
||||
}
|
||||
|
||||
handleDelete = (values) => {
|
||||
handleDelete = (values: any) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(this.props.t('rewrite_confirm_delete', { key: values.domain }))) {
|
||||
this.props.deleteRewrite(values);
|
||||
}
|
||||
};
|
||||
|
||||
handleSubmit = (values) => {
|
||||
handleSubmit = (values: any) => {
|
||||
const { modalType, currentRewrite } = this.props.rewrites;
|
||||
|
||||
if (modalType === MODAL_TYPE.EDIT_REWRITE && currentRewrite) {
|
||||
@ -36,7 +49,9 @@ class Rewrites extends Component {
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
|
||||
rewrites,
|
||||
|
||||
toggleRewritesModal,
|
||||
} = this.props;
|
||||
|
||||
@ -53,14 +68,9 @@ class Rewrites extends Component {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle
|
||||
title={t('dns_rewrites')}
|
||||
subtitle={t('rewrite_desc')}
|
||||
/>
|
||||
<Card
|
||||
id="rewrites"
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<PageTitle title={t('dns_rewrites')} subtitle={t('rewrite_desc')} />
|
||||
|
||||
<Card id="rewrites" bodyType="card-body box-body--settings">
|
||||
<Fragment>
|
||||
<Table
|
||||
list={list}
|
||||
@ -76,8 +86,7 @@ class Rewrites extends Component {
|
||||
type="button"
|
||||
className="btn btn-success btn-standard mt-3"
|
||||
onClick={() => toggleRewritesModal({ type: MODAL_TYPE.ADD_REWRITE })}
|
||||
disabled={processingAdd}
|
||||
>
|
||||
disabled={processingAdd}>
|
||||
<Trans>rewrite_add</Trans>
|
||||
</button>
|
||||
|
||||
@ -88,7 +97,6 @@ class Rewrites extends Component {
|
||||
handleSubmit={this.handleSubmit}
|
||||
processingAdd={processingAdd}
|
||||
processingDelete={processingDelete}
|
||||
processingUpdate={processingUpdate}
|
||||
currentRewrite={currentRewrite}
|
||||
/>
|
||||
</Fragment>
|
||||
@ -98,14 +106,4 @@ class Rewrites extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
@ -1,23 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import { toggleAllServices } from '../../../helpers/helpers';
|
||||
|
||||
import { renderServiceField } from '../../../helpers/form';
|
||||
import { FORM_NAME } from '../../../helpers/constants';
|
||||
|
||||
const Form = (props) => {
|
||||
const {
|
||||
blockedServices,
|
||||
handleSubmit,
|
||||
change,
|
||||
pristine,
|
||||
submitting,
|
||||
processing,
|
||||
processingSet,
|
||||
} = props;
|
||||
interface FormProps {
|
||||
blockedServices: unknown[];
|
||||
pristine: boolean;
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
change: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
processing: boolean;
|
||||
processingSet: boolean;
|
||||
t: (...args: unknown[]) => string;
|
||||
}
|
||||
|
||||
const Form = (props: FormProps) => {
|
||||
const { blockedServices, handleSubmit, change, pristine, submitting, processing, processingSet } = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
@ -28,24 +32,24 @@ const Form = (props) => {
|
||||
type="button"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={processing || processingSet}
|
||||
onClick={() => toggleAllServices(blockedServices, change, true)}
|
||||
>
|
||||
onClick={() => toggleAllServices(blockedServices, change, true)}>
|
||||
<Trans>block_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={processing || processingSet}
|
||||
onClick={() => toggleAllServices(blockedServices, change, false)}
|
||||
>
|
||||
onClick={() => toggleAllServices(blockedServices, change, false)}>
|
||||
<Trans>unblock_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="services">
|
||||
{blockedServices.map((service) => (
|
||||
{blockedServices.map((service: any) => (
|
||||
<Field
|
||||
key={service.id}
|
||||
icon={service.icon_svg}
|
||||
@ -63,8 +67,7 @@ const Form = (props) => {
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
disabled={submitting || pristine || processing || processingSet}
|
||||
>
|
||||
disabled={submitting || pristine || processing || processingSet}>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
@ -72,17 +75,6 @@ const Form = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
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([
|
||||
withTranslation(),
|
||||
reduxForm({
|
@ -1,10 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import { Timezone } from './Timezone';
|
||||
|
||||
import { TimeSelect } from './TimeSelect';
|
||||
|
||||
import { TimePeriod } from './TimePeriod';
|
||||
import { getFullDayName, getShortDayName } from './helpers';
|
||||
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
|
||||
@ -14,21 +16,26 @@ export const DAYS_OF_WEEK = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
const INITIAL_START_TIME_MS = 0;
|
||||
const INITIAL_END_TIME_MS = 86340000;
|
||||
|
||||
export const Modal = ({
|
||||
isOpen,
|
||||
currentDay,
|
||||
schedule,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}) => {
|
||||
interface ModalProps {
|
||||
schedule: {
|
||||
time_zone: string;
|
||||
};
|
||||
currentDay?: string;
|
||||
isOpen: boolean;
|
||||
onClose: (...args: unknown[]) => unknown;
|
||||
onSubmit: (values: any) => void;
|
||||
}
|
||||
|
||||
export const Modal = ({ isOpen, currentDay, schedule, onClose, onSubmit }: ModalProps) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const intialTimezone = schedule.time_zone === LOCAL_TIMEZONE_VALUE
|
||||
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
: schedule.time_zone;
|
||||
const intialTimezone =
|
||||
schedule.time_zone === LOCAL_TIMEZONE_VALUE
|
||||
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
: schedule.time_zone;
|
||||
|
||||
const [timezone, setTimezone] = useState(intialTimezone);
|
||||
const [days, setDays] = useState(new Set());
|
||||
const [days, setDays] = useState<Set<string>>(new Set());
|
||||
|
||||
const [startTime, setStartTime] = useState(INITIAL_START_TIME_MS);
|
||||
const [endTime, setEndTime] = useState(INITIAL_END_TIME_MS);
|
||||
@ -53,7 +60,7 @@ export const Modal = ({
|
||||
}
|
||||
}, [startTime, endTime]);
|
||||
|
||||
const addDays = (day) => {
|
||||
const addDays = (day: any) => {
|
||||
const newDays = new Set(days);
|
||||
|
||||
if (newDays.has(day)) {
|
||||
@ -65,11 +72,11 @@ export const Modal = ({
|
||||
setDays(newDays);
|
||||
};
|
||||
|
||||
const activeDay = (day) => {
|
||||
const activeDay = (day: any) => {
|
||||
return days.has(day);
|
||||
};
|
||||
|
||||
const onFormSubmit = (e) => {
|
||||
const onFormSubmit = (e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
const newSchedule = schedule;
|
||||
@ -93,23 +100,19 @@ export const Modal = ({
|
||||
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--schedule"
|
||||
closeTimeoutMS={0}
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
onRequestClose={onClose}>
|
||||
<div className="modal-content">
|
||||
<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}>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={onFormSubmit}>
|
||||
<div className="modal-body">
|
||||
<Timezone
|
||||
timezone={timezone}
|
||||
setTimezone={setTimezone}
|
||||
/>
|
||||
<Timezone timezone={timezone} setTimezone={setTimezone} />
|
||||
|
||||
<div className="schedule__days">
|
||||
{DAYS_OF_WEEK.map((day) => (
|
||||
@ -118,8 +121,7 @@ export const Modal = ({
|
||||
key={day}
|
||||
className="btn schedule__button-day"
|
||||
data-active={activeDay(day)}
|
||||
onClick={() => addDays(day)}
|
||||
>
|
||||
onClick={() => addDays(day)}>
|
||||
{getShortDayName(t, day)}
|
||||
</button>
|
||||
))}
|
||||
@ -127,69 +129,52 @@ export const Modal = ({
|
||||
|
||||
<div className="schedule__time-wrap">
|
||||
<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>
|
||||
|
||||
{wrongPeriod && (
|
||||
<div className="schedule__error">
|
||||
{t('schedule_invalid_select')}
|
||||
</div>
|
||||
)}
|
||||
{wrongPeriod && <div className="schedule__error">{t('schedule_invalid_select')}</div>}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<svg className="icons schedule__info-icon">
|
||||
<use xlinkHref="#calendar" />
|
||||
</svg>
|
||||
{days.size ? (
|
||||
Array.from(days).map((day) => getFullDayName(t, day)).join(', ')
|
||||
Array.from(days)
|
||||
.map((day) => getFullDayName(t, day))
|
||||
.join(', ')
|
||||
) : (
|
||||
<span>
|
||||
—
|
||||
</span>
|
||||
<span>—</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="schedule__info-row">
|
||||
<svg className="icons schedule__info-icon">
|
||||
<use xlinkHref="#watch" />
|
||||
</svg>
|
||||
{wrongPeriod ? (
|
||||
<span>
|
||||
—
|
||||
</span>
|
||||
<span>—</span>
|
||||
) : (
|
||||
<TimePeriod
|
||||
startTimeMs={startTime}
|
||||
endTimeMs={endTime}
|
||||
/>
|
||||
<TimePeriod startTimeMs={startTime} endTimeMs={endTime} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="schedule__notice">
|
||||
{t('schedule_modal_description')}
|
||||
</div>
|
||||
<div className="schedule__notice">{t('schedule_modal_description')}</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={days.size === 0 || wrongPeriod}
|
||||
onClick={onFormSubmit}
|
||||
>
|
||||
onClick={onFormSubmit}>
|
||||
{currentDay ? t('schedule_save') : t('schedule_add')}
|
||||
</button>
|
||||
</div>
|
||||
@ -199,11 +184,3 @@ export const Modal = ({
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.propTypes = {
|
||||
schedule: PropTypes.object.isRequired,
|
||||
currentDay: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
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>
|
||||
–
|
||||
<time>{endTime.hours}:{endTime.minutes}</time>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimePeriod.propTypes = {
|
||||
startTimeMs: PropTypes.number.isRequired,
|
||||
endTimeMs: PropTypes.number.isRequired,
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
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>
|
||||
–
|
||||
<time>
|
||||
{endTime.hours}:{endTime.minutes}
|
||||
</time>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,37 +1,35 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getTimeFromMs, convertTimeToMs } from './helpers';
|
||||
|
||||
export const TimeSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
interface TimeSelectProps {
|
||||
value: number;
|
||||
onChange: (time: number) => void;
|
||||
}
|
||||
|
||||
export const TimeSelect = ({ value, onChange }: TimeSelectProps) => {
|
||||
const { hours: initialHours, minutes: initialMinutes } = getTimeFromMs(value);
|
||||
|
||||
const [hours, setHours] = useState(initialHours);
|
||||
const [minutes, setMinutes] = useState(initialMinutes);
|
||||
|
||||
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 onHourChange = (event) => {
|
||||
const onHourChange = (event: any) => {
|
||||
setHours(event.target.value);
|
||||
onChange(convertTimeToMs(event.target.value, minutes));
|
||||
};
|
||||
|
||||
const onMinuteChange = (event) => {
|
||||
const onMinuteChange = (event: any) => {
|
||||
setMinutes(event.target.value);
|
||||
onChange(convertTimeToMs(hours, event.target.value));
|
||||
};
|
||||
|
||||
return (
|
||||
<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) => (
|
||||
<option key={hour} value={hour}>
|
||||
{hour}
|
||||
@ -39,11 +37,7 @@ export const TimeSelect = ({
|
||||
))}
|
||||
</select>
|
||||
:
|
||||
<select
|
||||
value={minutes}
|
||||
onChange={onMinuteChange}
|
||||
className="form-control custom-select"
|
||||
>
|
||||
<select value={minutes} onChange={onMinuteChange} className="form-control custom-select">
|
||||
{minuteOptions.map((minute) => (
|
||||
<option key={minute} value={minute}>
|
||||
{minute}
|
||||
@ -53,8 +47,3 @@ export const TimeSelect = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimeSelect.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import ct from 'countries-and-timezones';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
|
||||
|
||||
export const Timezone = ({
|
||||
timezone,
|
||||
setTimezone,
|
||||
}) => {
|
||||
interface TimezoneProps {
|
||||
timezone: string;
|
||||
setTimezone: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
export const Timezone = ({ timezone, setTimezone }: TimezoneProps) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const onTimeZoneChange = (event) => {
|
||||
const onTimeZoneChange = (event: any) => {
|
||||
setTimezone(event.target.value);
|
||||
};
|
||||
|
||||
@ -19,18 +20,10 @@ export const Timezone = ({
|
||||
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
<option value={LOCAL_TIMEZONE_VALUE}>
|
||||
{t('schedule_timezone')}
|
||||
</option>
|
||||
<select 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 */}
|
||||
{Object.keys(timezones).map((zone) => (
|
||||
<option key={zone} value={zone}>
|
||||
@ -41,8 +34,3 @@ export const Timezone = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Timezone.propTypes = {
|
||||
timezone: PropTypes.string.isRequired,
|
||||
setTimezone: PropTypes.func.isRequired,
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
export const getFullDayName = (t, abbreviation) => {
|
||||
export const getFullDayName = (t: any, abbreviation: any) => {
|
||||
const dayMap = {
|
||||
sun: t('sunday'),
|
||||
mon: t('monday'),
|
||||
@ -12,7 +12,7 @@ export const getFullDayName = (t, abbreviation) => {
|
||||
return dayMap[abbreviation] || '';
|
||||
};
|
||||
|
||||
export const getShortDayName = (t, abbreviation) => {
|
||||
export const getShortDayName = (t: any, abbreviation: any) => {
|
||||
const dayMap = {
|
||||
sun: t('sunday_short'),
|
||||
mon: t('monday_short'),
|
||||
@ -26,18 +26,19 @@ export const getShortDayName = (t, abbreviation) => {
|
||||
return dayMap[abbreviation] || '';
|
||||
};
|
||||
|
||||
export const getTimeFromMs = (value) => {
|
||||
export const getTimeFromMs = (value: any) => {
|
||||
const selectedTime = new Date(value);
|
||||
const hours = selectedTime.getUTCHours();
|
||||
const minutes = selectedTime.getUTCMinutes();
|
||||
|
||||
return {
|
||||
hours: hours.toString().padStart(2, '0'),
|
||||
|
||||
minutes: minutes.toString().padStart(2, '0'),
|
||||
};
|
||||
};
|
||||
|
||||
export const convertTimeToMs = (hours, minutes) => {
|
||||
export const convertTimeToMs = (hours: any, minutes: any) => {
|
||||
const selectedTime = new Date(0);
|
||||
selectedTime.setUTCHours(parseInt(hours, 10));
|
||||
selectedTime.setUTCMinutes(parseInt(minutes, 10));
|
@ -1,19 +1,23 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { Modal } from './Modal';
|
||||
import { getFullDayName, getShortDayName } from './helpers';
|
||||
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
|
||||
|
||||
import { TimePeriod } from './TimePeriod';
|
||||
import './styles.css';
|
||||
|
||||
export const ScheduleForm = ({
|
||||
schedule,
|
||||
onScheduleSubmit,
|
||||
clientForm,
|
||||
}) => {
|
||||
interface ScheduleFormProps {
|
||||
schedule?: {
|
||||
time_zone: string;
|
||||
};
|
||||
onScheduleSubmit: (values: any) => void;
|
||||
clientForm?: boolean;
|
||||
}
|
||||
|
||||
export const ScheduleForm = ({ schedule, onScheduleSubmit, clientForm }: ScheduleFormProps) => {
|
||||
const [t] = useTranslation();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [currentDay, setCurrentDay] = useState();
|
||||
@ -25,12 +29,12 @@ export const ScheduleForm = ({
|
||||
const scheduleMap = new Map();
|
||||
filteredScheduleKeys.forEach((day) => scheduleMap.set(day, schedule[day]));
|
||||
|
||||
const onSubmit = (values) => {
|
||||
const onSubmit = (values: any) => {
|
||||
onScheduleSubmit(values);
|
||||
onModalClose();
|
||||
};
|
||||
|
||||
const onDelete = (day) => {
|
||||
const onDelete = (day: any) => {
|
||||
scheduleMap.delete(day);
|
||||
|
||||
const scheduleWeek = Object.fromEntries(Array.from(scheduleMap.entries()));
|
||||
@ -41,7 +45,7 @@ export const ScheduleForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const onEdit = (day) => {
|
||||
const onEdit = (day: any) => {
|
||||
setCurrentDay(day);
|
||||
onModalOpen();
|
||||
};
|
||||
@ -67,23 +71,18 @@ export const ScheduleForm = ({
|
||||
|
||||
return (
|
||||
<div key={day} className="schedule__row">
|
||||
<div className="schedule__day">
|
||||
{getFullDayName(t, day)}
|
||||
</div>
|
||||
<div className="schedule__day schedule__day--mobile">
|
||||
{getShortDayName(t, day)}
|
||||
</div>
|
||||
<TimePeriod
|
||||
startTimeMs={data.start}
|
||||
endTimeMs={data.end}
|
||||
/>
|
||||
<div className="schedule__day">{getFullDayName(t, day)}</div>
|
||||
|
||||
<div className="schedule__day schedule__day--mobile">{getShortDayName(t, day)}</div>
|
||||
|
||||
<TimePeriod startTimeMs={data.start} endTimeMs={data.end} />
|
||||
|
||||
<div className="schedule__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-primary btn-sm schedule__button"
|
||||
title={t('edit_table_action')}
|
||||
onClick={() => onEdit(day)}
|
||||
>
|
||||
onClick={() => onEdit(day)}>
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#edit" />
|
||||
</svg>
|
||||
@ -93,8 +92,7 @@ export const ScheduleForm = ({
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm schedule__button"
|
||||
title={t('delete_table_action')}
|
||||
onClick={() => onDelete(day)}
|
||||
>
|
||||
onClick={() => onDelete(day)}>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
@ -112,8 +110,7 @@ export const ScheduleForm = ({
|
||||
{ 'btn-outline-success btn-sm': clientForm },
|
||||
{ 'btn-success btn-standard': !clientForm },
|
||||
)}
|
||||
onClick={onAdd}
|
||||
>
|
||||
onClick={onAdd}>
|
||||
{t('schedule_new')}
|
||||
</button>
|
||||
|
||||
@ -129,9 +126,3 @@ export const ScheduleForm = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ScheduleForm.propTypes = {
|
||||
schedule: PropTypes.object,
|
||||
onScheduleSubmit: PropTypes.func.isRequired,
|
||||
clientForm: PropTypes.bool,
|
||||
};
|
@ -73,7 +73,7 @@
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.schedule__button-day[data-active="true"] {
|
||||
.schedule__button-day[data-active='true'] {
|
||||
color: var(--btn-success-bgcolor);
|
||||
border-color: var(--btn-success-bgcolor);
|
||||
}
|
||||
|
@ -2,50 +2,63 @@ import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Form from './Form';
|
||||
|
||||
import Card from '../../ui/Card';
|
||||
import { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services';
|
||||
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
|
||||
import { ScheduleForm } from './ScheduleForm';
|
||||
import { RootState } from '../../../initialState';
|
||||
|
||||
const getInitialDataForServices = (initial) => (initial ? initial.reduce(
|
||||
(acc, service) => {
|
||||
acc.blocked_services[service] = true;
|
||||
return acc;
|
||||
}, { blocked_services: {} },
|
||||
) : initial);
|
||||
const getInitialDataForServices = (initial: any) =>
|
||||
initial
|
||||
? initial.reduce(
|
||||
(acc: any, service: any) => {
|
||||
acc.blocked_services[service] = true;
|
||||
return acc;
|
||||
},
|
||||
{ blocked_services: {} },
|
||||
)
|
||||
: initial;
|
||||
|
||||
const Services = () => {
|
||||
const [t] = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const services = useSelector((store) => store?.services);
|
||||
|
||||
const services = useSelector((state: RootState) => state.services);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getBlockedServices());
|
||||
dispatch(getAllBlockedServices());
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
const handleSubmit = (values: any) => {
|
||||
if (!values || !values.blocked_services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blocked_services = Object
|
||||
.keys(values.blocked_services)
|
||||
.filter((service) => values.blocked_services[service]);
|
||||
const blocked_services = Object.keys(values.blocked_services).filter(
|
||||
(service) => values.blocked_services[service],
|
||||
);
|
||||
|
||||
dispatch(updateBlockedServices({
|
||||
ids: blocked_services,
|
||||
schedule: services.list.schedule,
|
||||
}));
|
||||
dispatch(
|
||||
updateBlockedServices({
|
||||
ids: blocked_services,
|
||||
schedule: services.list.schedule,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleScheduleSubmit = (values) => {
|
||||
dispatch(updateBlockedServices({
|
||||
ids: services.list.ids,
|
||||
schedule: values,
|
||||
}));
|
||||
const handleScheduleSubmit = (values: any) => {
|
||||
dispatch(
|
||||
updateBlockedServices({
|
||||
ids: services.list.ids,
|
||||
schedule: values,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const initialValues = getInitialDataForServices(services.list.ids);
|
||||
@ -56,13 +69,9 @@ const Services = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title={t('blocked_services')}
|
||||
subtitle={t('blocked_services_desc')}
|
||||
/>
|
||||
<Card
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<PageTitle title={t('blocked_services')} subtitle={t('blocked_services_desc')} />
|
||||
|
||||
<Card bodyType="card-body box-body--settings">
|
||||
<div className="form">
|
||||
<Form
|
||||
initialValues={initialValues}
|
||||
@ -77,12 +86,8 @@ const Services = () => {
|
||||
<Card
|
||||
title={t('schedule_services')}
|
||||
subtitle={t('schedule_services_desc')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<ScheduleForm
|
||||
schedule={services.list.schedule}
|
||||
onScheduleSubmit={handleScheduleSubmit}
|
||||
/>
|
||||
bodyType="card-body box-body--settings">
|
||||
<ScheduleForm schedule={services.list.schedule} onScheduleSubmit={handleScheduleSubmit} />
|
||||
</Card>
|
||||
</>
|
||||
);
|
@ -1,17 +1,32 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @ts-expect-error FIXME: update react-table
|
||||
import ReactTable from 'react-table';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import CellWrap from '../ui/CellWrap';
|
||||
import { MODAL_TYPE } from '../../helpers/constants';
|
||||
|
||||
import { formatDetailedDateTime } from '../../helpers/helpers';
|
||||
|
||||
import { isValidAbsolutePath } from '../../helpers/form';
|
||||
import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from '../../helpers/localStorageHelper';
|
||||
|
||||
class Table extends Component {
|
||||
getDateCell = (row) => CellWrap(row, formatDetailedDateTime);
|
||||
interface TableProps {
|
||||
filters: unknown[];
|
||||
loading: boolean;
|
||||
processingConfigFilter: boolean;
|
||||
toggleFilteringModal: (...args: unknown[]) => unknown;
|
||||
handleDelete: (...args: unknown[]) => unknown;
|
||||
toggleFilter: (...args: unknown[]) => unknown;
|
||||
t: (...args: unknown[]) => string;
|
||||
whitelist?: boolean;
|
||||
}
|
||||
|
||||
renderCheckbox = ({ original }) => {
|
||||
class Table extends Component<TableProps> {
|
||||
getDateCell = (row: any) => CellWrap(row, formatDetailedDateTime);
|
||||
|
||||
renderCheckbox = ({ original }: any) => {
|
||||
const { processingConfigFilter, toggleFilter } = this.props;
|
||||
const { url, name, enabled } = original;
|
||||
const data = { name, url, enabled: !enabled };
|
||||
@ -25,6 +40,7 @@ class Table extends Component {
|
||||
checked={enabled}
|
||||
disabled={processingConfigFilter}
|
||||
/>
|
||||
|
||||
<span className="checkbox__label" />
|
||||
</label>
|
||||
);
|
||||
@ -50,17 +66,15 @@ class Table extends Component {
|
||||
accessor: 'url',
|
||||
minWidth: 180,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
Cell: ({ value }) => (
|
||||
Cell: ({ value }: any) => (
|
||||
<div className="logs__row">
|
||||
{isValidAbsolutePath(value) ? value
|
||||
: <a
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link logs__text"
|
||||
>
|
||||
{isValidAbsolutePath(value) ? (
|
||||
value
|
||||
) : (
|
||||
<a href={value} target="_blank" rel="noopener noreferrer" className="link logs__text">
|
||||
{value}
|
||||
</a>}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@ -69,7 +83,7 @@ class Table extends Component {
|
||||
accessor: 'rulesCount',
|
||||
className: 'text-center',
|
||||
minWidth: 100,
|
||||
Cell: (props) => props.value.toLocaleString(),
|
||||
Cell: (props: any) => props.value.toLocaleString(),
|
||||
},
|
||||
{
|
||||
Header: <Trans>last_time_updated_table_header</Trans>,
|
||||
@ -85,9 +99,10 @@ class Table extends Component {
|
||||
width: 100,
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
Cell: (row) => {
|
||||
Cell: (row: any) => {
|
||||
const { original } = row;
|
||||
const { url } = original;
|
||||
|
||||
const { t, toggleFilteringModal, handleDelete } = this.props;
|
||||
|
||||
return (
|
||||
@ -96,22 +111,22 @@ class Table extends Component {
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-primary btn-sm mr-2"
|
||||
title={t('edit_table_action')}
|
||||
onClick={() => toggleFilteringModal({
|
||||
type: MODAL_TYPE.EDIT_FILTERS,
|
||||
url,
|
||||
})
|
||||
}
|
||||
>
|
||||
onClick={() =>
|
||||
toggleFilteringModal({
|
||||
type: MODAL_TYPE.EDIT_FILTERS,
|
||||
url,
|
||||
})
|
||||
}>
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#edit" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||
onClick={() => handleDelete(url)}
|
||||
title={t('delete_table_action')}
|
||||
>
|
||||
title={t('delete_table_action')}>
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
@ -123,9 +138,7 @@ class Table extends Component {
|
||||
];
|
||||
|
||||
render() {
|
||||
const {
|
||||
loading, filters, t, whitelist,
|
||||
} = this.props;
|
||||
const { loading, filters, t, whitelist } = this.props;
|
||||
|
||||
const localStorageKey = whitelist
|
||||
? LOCAL_STORAGE_KEYS.ALLOWLIST_PAGE_SIZE
|
||||
@ -137,7 +150,7 @@ class Table extends Component {
|
||||
columns={this.columns}
|
||||
showPagination
|
||||
defaultPageSize={LocalStorageHelper.getItem(localStorageKey) || 10}
|
||||
onPageSizeChange={(size) => LocalStorageHelper.setItem(localStorageKey, size)}
|
||||
onPageSizeChange={(size: any) => LocalStorageHelper.setItem(localStorageKey, size)}
|
||||
loading={loading}
|
||||
minRows={6}
|
||||
ofText="/"
|
||||
@ -152,15 +165,4 @@ class Table extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
@ -1,10 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import enhanceWithClickOutside from 'react-click-outside';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants';
|
||||
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
|
||||
const MENU_ITEMS = [
|
||||
@ -80,7 +82,14 @@ const FILTERS_ITEMS = [
|
||||
},
|
||||
];
|
||||
|
||||
class Menu extends Component {
|
||||
interface MenuProps {
|
||||
isMenuOpen: boolean;
|
||||
closeMenu: (...args: unknown[]) => unknown;
|
||||
pathname: string;
|
||||
t?: (...args: unknown[]) => string;
|
||||
}
|
||||
|
||||
class Menu extends Component<MenuProps> {
|
||||
handleClickOutside = () => {
|
||||
this.props.closeMenu();
|
||||
};
|
||||
@ -89,52 +98,51 @@ class Menu extends Component {
|
||||
this.props.closeMenu();
|
||||
};
|
||||
|
||||
getActiveClassForDropdown = (URLS) => {
|
||||
getActiveClassForDropdown = (URLS: any) => {
|
||||
const isActivePage = Object.values(URLS)
|
||||
.some((item) => item === this.props.pathname);
|
||||
|
||||
.some((item: any) => item === this.props.pathname);
|
||||
|
||||
return isActivePage ? 'active' : '';
|
||||
};
|
||||
|
||||
getNavLink = ({
|
||||
route, exact, text, order, className, icon,
|
||||
}) => (
|
||||
getNavLink = ({ route, exact, text, order, className, icon }: any) => (
|
||||
<NavLink
|
||||
to={route}
|
||||
key={route}
|
||||
exact={exact || false}
|
||||
className={`order-${order} ${className}`}
|
||||
onClick={this.closeMenu}
|
||||
>
|
||||
onClick={this.closeMenu}>
|
||||
{icon && (
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref={`#${icon}`} />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
<Trans>{text}</Trans>
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
getDropdown = ({
|
||||
label, order, URLS, icon, ITEMS,
|
||||
}) => (
|
||||
getDropdown = ({ label, order, URLS, icon, ITEMS }: any) => (
|
||||
<Dropdown
|
||||
label={this.props.t(label)}
|
||||
baseClassName='dropdown'
|
||||
baseClassName="dropdown"
|
||||
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
|
||||
icon={icon}>
|
||||
{ITEMS.map((item) => (
|
||||
{ITEMS.map((item: any) =>
|
||||
this.getNavLink({
|
||||
...item,
|
||||
order,
|
||||
className: 'dropdown-item',
|
||||
})))}
|
||||
}),
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
render() {
|
||||
const menuClass = classnames({
|
||||
'header__column mobile-menu': true,
|
||||
|
||||
'mobile-menu--active': this.props.isMenuOpen,
|
||||
});
|
||||
return (
|
||||
@ -142,17 +150,14 @@ class Menu extends Component {
|
||||
<div className={menuClass}>
|
||||
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
|
||||
{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({
|
||||
...item,
|
||||
className: 'nav-link',
|
||||
})}
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className="nav-item order-1">
|
||||
{this.getDropdown({
|
||||
order: 1,
|
||||
@ -162,6 +167,7 @@ class Menu extends Component {
|
||||
ITEMS: SETTINGS_ITEMS,
|
||||
})}
|
||||
</li>
|
||||
|
||||
<li className="nav-item order-2">
|
||||
{this.getDropdown({
|
||||
order: 2,
|
||||
@ -178,11 +184,4 @@ class Menu extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
Menu.propTypes = {
|
||||
isMenuOpen: PropTypes.bool.isRequired,
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
pathname: PropTypes.string.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default withTranslation()(enhanceWithClickOutside(Menu));
|
@ -1,75 +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.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;
|
74
client/src/components/Header/index.tsx
Normal file
74
client/src/components/Header/index.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
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;
|
@ -1,13 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { HashLink as Link } from 'react-router-hash-link';
|
||||
|
||||
const AnonymizerNotification = () => (
|
||||
<div className="alert alert-primary mt-6">
|
||||
<Trans components={[
|
||||
<strong key="0">text</strong>,
|
||||
<Link to="/settings#logs-config" key="1">link</Link>,
|
||||
]}>
|
||||
<Trans
|
||||
components={[
|
||||
<strong key="0">text</strong>,
|
||||
|
||||
<Link to="/settings#logs-config" key="1">
|
||||
link
|
||||
</Link>,
|
||||
]}>
|
||||
anonymizer_notification
|
||||
</Trans>
|
||||
</div>
|
@ -3,36 +3,55 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
||||
|
||||
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
|
||||
|
||||
import IconTooltip from './IconTooltip';
|
||||
|
||||
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
|
||||
import { toggleClientBlock } from '../../../actions/access';
|
||||
import { getBlockClientInfo } from './helpers';
|
||||
import { getStats } from '../../../actions/stats';
|
||||
import { updateLogs } from '../../../actions/queryLogs';
|
||||
import { RootState } from '../../../initialState';
|
||||
|
||||
const ClientCell = ({
|
||||
client,
|
||||
client_id,
|
||||
client_info,
|
||||
domain,
|
||||
reason,
|
||||
}) => {
|
||||
interface ClientCellProps {
|
||||
client: string;
|
||||
client_id?: string;
|
||||
client_info?: {
|
||||
name: string;
|
||||
whois: {
|
||||
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 dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
|
||||
|
||||
const autoClients = useSelector((state: RootState) => state.dashboard.autoClients, 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 autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const clients = useSelector((state) => state.dashboard.clients);
|
||||
const autoClient = autoClients.find((autoClient: any) => autoClient.name === client);
|
||||
|
||||
const clients = useSelector((state: RootState) => state.dashboard.clients);
|
||||
const source = autoClient?.source;
|
||||
const whoisAvailable = client_info && Object.keys(client_info.whois).length > 0;
|
||||
const clientName = client_info?.name || client_id;
|
||||
@ -57,7 +76,7 @@ const ClientCell = ({
|
||||
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const clientIds = clients.map((c) => c.ids).flat();
|
||||
const clientIds = clients.map((c: any) => c.ids).flat();
|
||||
|
||||
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
|
||||
'mt-2': isDetailed && !client_info?.name && !whoisAvailable,
|
||||
@ -68,7 +87,7 @@ const ClientCell = ({
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
const renderBlockingButton = (isFiltered, domain) => {
|
||||
const renderBlockingButton = (isFiltered: any, domain: any) => {
|
||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
|
||||
const {
|
||||
@ -79,7 +98,7 @@ const ClientCell = ({
|
||||
client,
|
||||
client_info?.disallowed || false,
|
||||
client_info?.disallowed_rule || '',
|
||||
allowedСlients,
|
||||
allowedClients,
|
||||
);
|
||||
|
||||
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
|
||||
@ -108,11 +127,13 @@ const ClientCell = ({
|
||||
name: blockingClientKey,
|
||||
onClick: async () => {
|
||||
if (window.confirm(confirmMessage)) {
|
||||
await dispatch(toggleClientBlock(
|
||||
client,
|
||||
client_info?.disallowed || false,
|
||||
client_info?.disallowed_rule || '',
|
||||
));
|
||||
await dispatch(
|
||||
toggleClientBlock(
|
||||
client,
|
||||
client_info?.disallowed || false,
|
||||
client_info?.disallowed_rule || '',
|
||||
),
|
||||
);
|
||||
await dispatch(updateLogs());
|
||||
setOptionsOpened(false);
|
||||
}
|
||||
@ -130,21 +151,19 @@ const ClientCell = ({
|
||||
});
|
||||
}
|
||||
|
||||
const getOptions = (options) => {
|
||||
const getOptions = (options: any) => {
|
||||
if (options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.map(({
|
||||
name, onClick, disabled, className,
|
||||
}) => (
|
||||
{options.map(({ name, onClick, disabled, className }: any) => (
|
||||
<button
|
||||
key={name}
|
||||
className={classNames('button-action--arrow-option px-4 py-1', className)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
disabled={disabled}>
|
||||
{t(name)}
|
||||
</button>
|
||||
))}
|
||||
@ -160,11 +179,7 @@ const ClientCell = ({
|
||||
|
||||
return (
|
||||
<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">
|
||||
<use xlinkHref="#bullets" />
|
||||
</svg>
|
||||
@ -188,10 +203,7 @@ const ClientCell = ({
|
||||
};
|
||||
|
||||
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
|
||||
className={hintClass}
|
||||
columnClass="grid grid--limited"
|
||||
@ -202,6 +214,7 @@ const ClientCell = ({
|
||||
content={processedData}
|
||||
placement="bottom"
|
||||
/>
|
||||
|
||||
<div className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>
|
||||
{renderFormattedClientCell(client, clientInfo, isDetailed, true)}
|
||||
@ -210,8 +223,7 @@ const ClientCell = ({
|
||||
<Link
|
||||
className="detailed-info d-none d-sm-block logs__text logs__text--link logs__text--client"
|
||||
to={`logs?search="${encodeURIComponent(clientName)}"`}
|
||||
title={clientName}
|
||||
>
|
||||
title={clientName}>
|
||||
{clientName}
|
||||
</Link>
|
||||
)}
|
||||
@ -221,21 +233,4 @@ const ClientCell = ({
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
@ -1,29 +0,0 @@
|
||||
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;
|
37
client/src/components/Logs/Cells/DateCell.tsx
Normal file
37
client/src/components/Logs/Cells/DateCell.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
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;
|
@ -1,16 +1,32 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import propTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
LONG_TIME_FORMAT,
|
||||
SCHEME_TO_PROTOCOL_MAP,
|
||||
} from '../../../helpers/constants';
|
||||
|
||||
import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';
|
||||
import { getSourceData } from '../../../helpers/trackers/trackers';
|
||||
|
||||
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 = ({
|
||||
answer_dnssec,
|
||||
@ -21,10 +37,12 @@ const DomainCell = ({
|
||||
tracker,
|
||||
type,
|
||||
ecs,
|
||||
}) => {
|
||||
}: DomainCellProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
const dnssec_enabled = useSelector((state: RootState) => state.dnsConfig.dnssec_enabled);
|
||||
|
||||
const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed);
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
@ -43,7 +61,15 @@ const DomainCell = ({
|
||||
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
|
||||
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),
|
||||
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
||||
domain,
|
||||
@ -76,24 +102,16 @@ const DomainCell = ({
|
||||
name_table_header: tracker?.name,
|
||||
category_label: hasTracker && captitalizeWords(tracker.category),
|
||||
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}
|
||||
</a>
|
||||
),
|
||||
};
|
||||
|
||||
const renderGrid = (content, idx) => {
|
||||
const renderGrid = (content: any, idx: any) => {
|
||||
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 (
|
||||
<div key={idx} className={className}>
|
||||
@ -102,10 +120,11 @@ const DomainCell = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getGrid = (contentObj, title, className) => [
|
||||
const getGrid = (contentObj: any, title: string, className?: string) => [
|
||||
<div key={title} className={classNames('pb-2 grid--title', className)}>
|
||||
{t(title)}
|
||||
</div>,
|
||||
|
||||
<div key={`${title}-1`} className="grid grid--limited">
|
||||
{React.Children.map(Object.entries(contentObj), renderGrid)}
|
||||
</div>,
|
||||
@ -113,7 +132,9 @@ const DomainCell = ({
|
||||
|
||||
const requestDetails = getGrid(requestDetailsObj, 'request_details');
|
||||
|
||||
const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails;
|
||||
const renderContent = hasTracker
|
||||
? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4'))
|
||||
: requestDetails;
|
||||
|
||||
const valueClass = classNames('w-100 text-truncate', {
|
||||
'px-2 d-flex justify-content-center flex-column': isDetailed,
|
||||
@ -122,10 +143,7 @@ const DomainCell = ({
|
||||
const details = [ip, protocol].filter(Boolean).join(', ');
|
||||
|
||||
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 && (
|
||||
<IconTooltip
|
||||
className={lockIconClass}
|
||||
@ -137,14 +155,16 @@ const DomainCell = ({
|
||||
placement="bottom"
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconTooltip
|
||||
className={privacyIconClass}
|
||||
tooltipClass="pt-4 pb-5 px-5 mw-75"
|
||||
xlinkHref="privacy"
|
||||
contentItemClass="key-colon"
|
||||
renderContent={renderContent}
|
||||
place="bottom"
|
||||
placement="bottom"
|
||||
/>
|
||||
|
||||
<div className={valueClass}>
|
||||
{unicodeName ? (
|
||||
<div className="text-truncate overflow-break-mobile" title={unicodeName}>
|
||||
@ -156,10 +176,7 @@ const DomainCell = ({
|
||||
</div>
|
||||
)}
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
@ -168,15 +185,4 @@ 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;
|
@ -1,54 +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';
|
||||
|
||||
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;
|
73
client/src/components/Logs/Cells/Header.tsx
Normal file
73
client/src/components/Logs/Cells/Header.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
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;
|
@ -1,22 +0,0 @@
|
||||
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;
|
23
client/src/components/Logs/Cells/HeaderCell.tsx
Normal file
23
client/src/components/Logs/Cells/HeaderCell.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
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;
|
@ -91,18 +91,24 @@
|
||||
margin: -0.5rem 0 0;
|
||||
}
|
||||
|
||||
.grid > .key__time_table_header, .grid > .key__data, .grid > .key__encryption_status, .grid > .key__elapsed {
|
||||
.grid > .key__time_table_header,
|
||||
.grid > .key__data,
|
||||
.grid > .key__encryption_status,
|
||||
.grid > .key__elapsed {
|
||||
grid-column: 1 / span 1;
|
||||
}
|
||||
|
||||
.grid > .value__time_table_header, .grid > .value__data, .grid > .value__encryption_status, .grid > .value__elapsed {
|
||||
.grid > .value__time_table_header,
|
||||
.grid > .value__data,
|
||||
.grid > .value__encryption_status,
|
||||
.grid > .value__elapsed {
|
||||
grid-column: 2 / span 1;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.grid .key-colon:nth-child(odd)::after {
|
||||
content: ":";
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.grid__one-row {
|
||||
@ -127,7 +133,7 @@
|
||||
}
|
||||
|
||||
.title--border:before {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
border-top: 0.5px solid var(--gray-d8) !important;
|
||||
|
@ -1,76 +0,0 @@
|
||||
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;
|
94
client/src/components/Logs/Cells/IconTooltip.tsx
Normal file
94
client/src/components/Logs/Cells/IconTooltip.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
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;
|
@ -1,141 +0,0 @@
|
||||
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;
|
150
client/src/components/Logs/Cells/ResponseCell.tsx
Normal file
150
client/src/components/Logs/Cells/ResponseCell.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
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;
|
@ -2,20 +2,20 @@ import i18next from 'i18next';
|
||||
|
||||
export const BUTTON_PREFIX = 'btn_';
|
||||
|
||||
export const getBlockClientInfo = (ip, disallowed, disallowed_rule, allowedСlients) => {
|
||||
export const getBlockClientInfo = (ip: any, disallowed: any, disallowed_rule: any, allowedClients: any) => {
|
||||
let confirmMessage;
|
||||
|
||||
if (disallowed) {
|
||||
confirmMessage = i18next.t('client_confirm_unblock', { ip: disallowed_rule || ip });
|
||||
} else {
|
||||
confirmMessage = `${i18next.t('adg_will_drop_dns_queries')} ${i18next.t('client_confirm_block', { ip })}`;
|
||||
if (allowedСlients.length > 0) {
|
||||
if (allowedClients.length > 0) {
|
||||
confirmMessage = confirmMessage.concat(`\n\n${i18next.t('filter_allowlist', { disallowed_rule })}`);
|
||||
}
|
||||
}
|
||||
|
||||
const buttonKey = i18next.t(disallowed ? 'allow_this_client' : 'disallow_this_client');
|
||||
const lastRuleInAllowlist = !disallowed && allowedСlients === disallowed_rule;
|
||||
const lastRuleInAllowlist = !disallowed && allowedClients === disallowed_rule;
|
||||
|
||||
return {
|
||||
confirmMessage,
|
@ -1,285 +0,0 @@
|
||||
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;
|
306
client/src/components/Logs/Cells/index.tsx
Normal file
306
client/src/components/Logs/Cells/index.tsx
Normal file
@ -0,0 +1,306 @@
|
||||
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;
|
@ -1,5 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { HashLink as Link } from 'react-router-hash-link';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
@ -11,6 +12,7 @@ const Disabled = () => (
|
||||
<Trans>query_log</Trans>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="lead text-center py-6">
|
||||
<Trans
|
||||
@ -18,8 +20,7 @@ const Disabled = () => (
|
||||
<Link to="/settings#logs-config" key="0">
|
||||
link
|
||||
</Link>,
|
||||
]}
|
||||
>
|
||||
]}>
|
||||
query_log_disabled
|
||||
</Trans>
|
||||
</div>
|
@ -1,200 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
DEBOUNCE_FILTER_TIMEOUT,
|
||||
DEFAULT_LOGS_FILTER,
|
||||
FORM_NAME,
|
||||
RESPONSE_FILTER,
|
||||
RESPONSE_FILTER_QUERIES,
|
||||
} from '../../../helpers/constants';
|
||||
import { setLogsFilter } from '../../../actions/queryLogs';
|
||||
import useDebounce from '../../../helpers/useDebounce';
|
||||
import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers';
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
|
||||
const renderFilterField = ({
|
||||
input,
|
||||
id,
|
||||
className,
|
||||
placeholder,
|
||||
type,
|
||||
disabled,
|
||||
autoComplete,
|
||||
tooltip,
|
||||
meta: { touched, error },
|
||||
onClearInputClick,
|
||||
onKeyDown,
|
||||
normalizeOnBlur,
|
||||
}) => {
|
||||
const onBlur = (event) => createOnBlurHandler(event, input, normalizeOnBlur);
|
||||
|
||||
return <>
|
||||
<div className="input-group-search input-group-search__icon--magnifier">
|
||||
<svg className="icons icon--24 icon--gray">
|
||||
<use xlinkHref="#magnifier" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
{...input}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
aria-label={placeholder}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<div
|
||||
className={classNames('input-group-search input-group-search__icon--cross', { invisible: input.value.length < 1 })}>
|
||||
<svg className="icons icon--20 icon--gray" onClick={onClearInputClick}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="input-group-search input-group-search__icon--tooltip">
|
||||
<Tooltip content={tooltip} className="tooltip-container">
|
||||
<svg className="icons icon--20 icon--gray">
|
||||
<use xlinkHref="#question" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</span>
|
||||
{!disabled
|
||||
&& touched
|
||||
&& (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</>;
|
||||
};
|
||||
|
||||
renderFilterField.propTypes = {
|
||||
input: PropTypes.object.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
onClearInputClick: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
disabled: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
onKeyDown: PropTypes.func,
|
||||
normalizeOnBlur: PropTypes.func,
|
||||
meta: PropTypes.shape({
|
||||
touched: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
const FORM_NAMES = {
|
||||
search: 'search',
|
||||
response_status: 'response_status',
|
||||
};
|
||||
|
||||
const Form = (props) => {
|
||||
const {
|
||||
className = '',
|
||||
responseStatusClass,
|
||||
setIsLoading,
|
||||
change,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
response_status, search,
|
||||
} = useSelector((state) => state?.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||
|
||||
const [
|
||||
debouncedSearch,
|
||||
setDebouncedSearch,
|
||||
] = useDebounce(search.trim(), DEBOUNCE_FILTER_TIMEOUT);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setLogsFilter({
|
||||
response_status,
|
||||
search: debouncedSearch,
|
||||
}));
|
||||
|
||||
history.replace(`${getLogsUrlParams(debouncedSearch, response_status)}`);
|
||||
}, [response_status, debouncedSearch]);
|
||||
|
||||
if (response_status && !(response_status in RESPONSE_FILTER_QUERIES)) {
|
||||
change(FORM_NAMES.response_status, DEFAULT_LOGS_FILTER[FORM_NAMES.response_status]);
|
||||
}
|
||||
|
||||
const onInputClear = async () => {
|
||||
setIsLoading(true);
|
||||
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onEnterPress = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setDebouncedSearch(search);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeOnBlur = (data) => data.trim();
|
||||
|
||||
return (
|
||||
<form
|
||||
className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="field__search">
|
||||
<Field
|
||||
id={FORM_NAMES.search}
|
||||
name={FORM_NAMES.search}
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||
placeholder={t('domain_or_client')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onClearInputClick={onInputClear}
|
||||
onKeyDown={onEnterPress}
|
||||
normalizeOnBlur={normalizeOnBlur}
|
||||
/>
|
||||
</div>
|
||||
<div className="field__select">
|
||||
<Field
|
||||
name={FORM_NAMES.response_status}
|
||||
component="select"
|
||||
className={classNames('form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent', responseStatusClass)}
|
||||
>
|
||||
{Object.values(RESPONSE_FILTER)
|
||||
.map(({
|
||||
QUERY, LABEL, disabled,
|
||||
}) => (
|
||||
<option
|
||||
key={LABEL}
|
||||
value={QUERY}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t(LABEL)}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</Field>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
className: PropTypes.string,
|
||||
responseStatusClass: PropTypes.string,
|
||||
change: PropTypes.func.isRequired,
|
||||
setIsLoading: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default reduxForm({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
enableReinitialize: true,
|
||||
})(Form);
|
201
client/src/components/Logs/Filters/Form.tsx
Normal file
201
client/src/components/Logs/Filters/Form.tsx
Normal file
@ -0,0 +1,201 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
DEBOUNCE_FILTER_TIMEOUT,
|
||||
DEFAULT_LOGS_FILTER,
|
||||
FORM_NAME,
|
||||
RESPONSE_FILTER,
|
||||
RESPONSE_FILTER_QUERIES,
|
||||
} from '../../../helpers/constants';
|
||||
import { setLogsFilter } from '../../../actions/queryLogs';
|
||||
import useDebounce from '../../../helpers/useDebounce';
|
||||
|
||||
import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers';
|
||||
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
import { RootState } from '../../../initialState';
|
||||
|
||||
interface renderFilterFieldProps {
|
||||
input: {
|
||||
value: string;
|
||||
};
|
||||
id: string;
|
||||
onClearInputClick: (...args: unknown[]) => unknown;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
disabled?: boolean;
|
||||
autoComplete?: string;
|
||||
tooltip?: string;
|
||||
onKeyDown?: (...args: unknown[]) => unknown;
|
||||
normalizeOnBlur?: (...args: unknown[]) => unknown;
|
||||
meta: {
|
||||
touched?: boolean;
|
||||
error?: object;
|
||||
};
|
||||
}
|
||||
|
||||
const renderFilterField = ({
|
||||
input,
|
||||
id,
|
||||
className,
|
||||
placeholder,
|
||||
type,
|
||||
disabled,
|
||||
autoComplete,
|
||||
tooltip,
|
||||
meta: { touched, error },
|
||||
onClearInputClick,
|
||||
onKeyDown,
|
||||
normalizeOnBlur,
|
||||
}: renderFilterFieldProps) => {
|
||||
const onBlur = (event: any) => createOnBlurHandler(event, input, normalizeOnBlur);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="input-group-search input-group-search__icon--magnifier">
|
||||
<svg className="icons icon--24 icon--gray">
|
||||
<use xlinkHref="#magnifier" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<input
|
||||
{...input}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
aria-label={placeholder}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classNames('input-group-search input-group-search__icon--cross', {
|
||||
invisible: input.value.length < 1,
|
||||
})}>
|
||||
<svg className="icons icon--20 icon--gray" onClick={onClearInputClick}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<span className="input-group-search input-group-search__icon--tooltip">
|
||||
<Tooltip content={tooltip} className="tooltip-container">
|
||||
<svg className="icons icon--20 icon--gray">
|
||||
<use xlinkHref="#question" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</span>
|
||||
{!disabled && touched && error && <span className="form__message form__message--error">{error}</span>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FORM_NAMES = {
|
||||
search: 'search',
|
||||
response_status: 'response_status',
|
||||
};
|
||||
|
||||
interface FiltersFormProps {
|
||||
className?: string;
|
||||
responseStatusClass?: string;
|
||||
change: (...args: unknown[]) => unknown;
|
||||
setIsLoading?: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
const Form = (props: FiltersFormProps) => {
|
||||
const { className = '', responseStatusClass, setIsLoading, change } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const { response_status, search } = useSelector(
|
||||
(state: RootState) => state?.form[FORM_NAME.LOGS_FILTER].values,
|
||||
shallowEqual,
|
||||
);
|
||||
|
||||
const [debouncedSearch, setDebouncedSearch] = useDebounce(search.trim(), DEBOUNCE_FILTER_TIMEOUT);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
setLogsFilter({
|
||||
response_status,
|
||||
search: debouncedSearch,
|
||||
}),
|
||||
);
|
||||
|
||||
history.replace(`${getLogsUrlParams(debouncedSearch, response_status)}`);
|
||||
}, [response_status, debouncedSearch]);
|
||||
|
||||
if (response_status && !(response_status in RESPONSE_FILTER_QUERIES)) {
|
||||
change(FORM_NAMES.response_status, DEFAULT_LOGS_FILTER[FORM_NAMES.response_status]);
|
||||
}
|
||||
|
||||
const onInputClear = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onEnterPress = (e: any) => {
|
||||
if (e.key === 'Enter') {
|
||||
setDebouncedSearch(search);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeOnBlur = (data: any) => data.trim();
|
||||
|
||||
return (
|
||||
<form
|
||||
className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}>
|
||||
<div className="field__search">
|
||||
<Field
|
||||
id={FORM_NAMES.search}
|
||||
name={FORM_NAMES.search}
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||
placeholder={t('domain_or_client')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onClearInputClick={onInputClear}
|
||||
onKeyDown={onEnterPress}
|
||||
normalizeOnBlur={normalizeOnBlur}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field__select">
|
||||
<Field
|
||||
name={FORM_NAMES.response_status}
|
||||
component="select"
|
||||
className={classNames(
|
||||
'form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent',
|
||||
responseStatusClass,
|
||||
)}>
|
||||
{Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => (
|
||||
<option key={LABEL} value={QUERY} disabled={disabled}>
|
||||
{t(LABEL)}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
enableReinitialize: true,
|
||||
})(Form);
|
@ -1,12 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import Form from './Form';
|
||||
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../../actions/toasts';
|
||||
|
||||
const Filters = ({ filter, setIsLoading }) => {
|
||||
interface FiltersProps {
|
||||
filter: object;
|
||||
processingGetLogs: boolean;
|
||||
setIsLoading: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
const Filters = ({ filter, setIsLoading }: FiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@ -17,32 +23,29 @@ const Filters = ({ filter, setIsLoading }) => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return <div className="page-header page-header--logs">
|
||||
<h1 className="page-title page-title--large">
|
||||
{t('query_log')}
|
||||
<button
|
||||
return (
|
||||
<div className="page-header page-header--logs">
|
||||
<h1 className="page-title page-title--large">
|
||||
{t('query_log')}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon--green logs__refresh"
|
||||
title={t('refresh_btn')}
|
||||
onClick={refreshLogs}
|
||||
>
|
||||
<svg className="icons icon--24">
|
||||
<use xlinkHref="#update" />
|
||||
</svg>
|
||||
</button>
|
||||
</h1>
|
||||
<Form
|
||||
responseStatusClass="d-sm-block"
|
||||
initialValues={filter}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
onClick={refreshLogs}>
|
||||
<svg className="icons icon--24">
|
||||
<use xlinkHref="#update" />
|
||||
</svg>
|
||||
</button>
|
||||
</h1>
|
||||
|
||||
Filters.propTypes = {
|
||||
filter: PropTypes.object.isRequired,
|
||||
processingGetLogs: PropTypes.bool.isRequired,
|
||||
setIsLoading: PropTypes.func.isRequired,
|
||||
<Form
|
||||
// responseStatusClass="d-sm-block"
|
||||
// setIsLoading={setIsLoading}
|
||||
initialValues={filter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filters;
|
@ -1,18 +1,27 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import propTypes from 'prop-types';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
|
||||
import Header from './Cells/Header';
|
||||
import { getLogs } from '../../actions/queryLogs';
|
||||
|
||||
import Row from './Cells';
|
||||
|
||||
import { isScrolledIntoView } from '../../helpers/helpers';
|
||||
import { QUERY_LOGS_PAGE_LIMIT } from '../../helpers/constants';
|
||||
import { RootState } from '../../initialState';
|
||||
|
||||
interface InfiniteTableProps {
|
||||
isLoading: boolean;
|
||||
items: unknown[];
|
||||
isSmallScreen: boolean;
|
||||
setDetailedDataCurrent: Dispatch<SetStateAction<any>>;
|
||||
setButtonType: (...args: unknown[]) => unknown;
|
||||
setModalOpened: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
const InfiniteTable = ({
|
||||
isLoading,
|
||||
@ -21,14 +30,15 @@ const InfiniteTable = ({
|
||||
setDetailedDataCurrent,
|
||||
setButtonType,
|
||||
setModalOpened,
|
||||
}) => {
|
||||
}: InfiniteTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const loader = useRef(null);
|
||||
const loadingRef = useRef(null);
|
||||
|
||||
const isEntireLog = useSelector((state) => state.queryLogs.isEntireLog);
|
||||
const processingGetLogs = useSelector((state) => state.queryLogs.processingGetLogs);
|
||||
const isEntireLog = useSelector((state: RootState) => state.queryLogs.isEntireLog);
|
||||
|
||||
const processingGetLogs = useSelector((state: RootState) => state.queryLogs.processingGetLogs);
|
||||
const loading = isLoading || processingGetLogs;
|
||||
|
||||
const listener = useCallback(() => {
|
||||
@ -55,20 +65,23 @@ const InfiniteTable = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderRow = (row, idx) => <Row
|
||||
key={idx}
|
||||
rowProps={row}
|
||||
isSmallScreen={isSmallScreen}
|
||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
/>;
|
||||
const renderRow = (row: any, idx: any) => (
|
||||
<Row
|
||||
key={idx}
|
||||
rowProps={row}
|
||||
isSmallScreen={isSmallScreen}
|
||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
/>
|
||||
);
|
||||
|
||||
const isNothingFound = items.length === 0 && !processingGetLogs;
|
||||
|
||||
return (
|
||||
<div className="logs__table" role="grid">
|
||||
{loading && <Loading />}
|
||||
|
||||
<Header />
|
||||
{isNothingFound ? (
|
||||
<label className="logs__no-data">{t('nothing_found')}</label>
|
||||
@ -86,13 +99,4 @@ const InfiniteTable = ({
|
||||
);
|
||||
};
|
||||
|
||||
InfiniteTable.propTypes = {
|
||||
isLoading: propTypes.bool.isRequired,
|
||||
items: propTypes.array.isRequired,
|
||||
isSmallScreen: propTypes.bool.isRequired,
|
||||
setDetailedDataCurrent: propTypes.func.isRequired,
|
||||
setButtonType: propTypes.func.isRequired,
|
||||
setModalOpened: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default InfiniteTable;
|
@ -24,7 +24,7 @@
|
||||
--option-border-radius: 4px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
[data-theme='dark'] {
|
||||
--red: rgba(223, 56, 18, 0.25);
|
||||
--green-pale: rgba(103, 178, 121, 0.25);
|
||||
--yellow: rgba(247, 181, 0, 0.2);
|
||||
@ -42,11 +42,11 @@
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs__text a {
|
||||
[data-theme='dark'] .logs__text a {
|
||||
color: var(--gray-f3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs__text a:hover {
|
||||
[data-theme='dark'] .logs__text a:hover {
|
||||
color: var(--gray-f3);
|
||||
}
|
||||
|
||||
@ -74,9 +74,9 @@
|
||||
color: #295a9f;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs__text--link,
|
||||
[data-theme="dark"] .logs__text--link:hover,
|
||||
[data-theme="dark"] .logs__text--link:focus {
|
||||
[data-theme='dark'] .logs__text--link,
|
||||
[data-theme='dark'] .logs__text--link:hover,
|
||||
[data-theme='dark'] .logs__text--link:focus {
|
||||
color: var(--gray-f3);
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
[data-theme=dark] .icon--selected {
|
||||
[data-theme='dark'] .icon--selected {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@
|
||||
}
|
||||
|
||||
.custom-select__arrow--left {
|
||||
background: var(--white) url("../ui/svg/chevron-down.svg") no-repeat;
|
||||
background: var(--white) url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5YWEwYWMiCiAgICAgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hldnJvbi1kb3duIj4KICAgIDxwb2x5bGluZSBwb2ludHM9IjYgOSAxMiAxNSAxOCA5Ij48L3BvbHlsaW5lPgo8L3N2Zz4K") no-repeat;
|
||||
background-position: 5px 9px;
|
||||
background-size: 22px;
|
||||
}
|
||||
@ -150,7 +150,7 @@
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .form-control--transparent option {
|
||||
[data-theme='dark'] .form-control--transparent option {
|
||||
background-color: var(--card-bgcolor);
|
||||
}
|
||||
|
||||
@ -351,7 +351,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tooltip-custom__container .button-action--arrow-option:not(:disabled):hover {
|
||||
[data-theme='dark'] .tooltip-custom__container .button-action--arrow-option:not(:disabled):hover {
|
||||
background: var(--ctrl-dropdown-bgcolor-focus);
|
||||
}
|
||||
|
||||
@ -387,7 +387,7 @@
|
||||
background-color: var(--logs__row--blue-bgcolor);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs__row--blue .logs__text--link {
|
||||
[data-theme='dark'] .logs__row--blue .logs__text--link {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
@ -465,13 +465,13 @@
|
||||
}
|
||||
|
||||
.logs__whois::after {
|
||||
content: "|";
|
||||
content: '|';
|
||||
padding: 0 5px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.logs__whois:last-child::after {
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
|
||||
.logs__whois-icon.icons {
|
||||
@ -501,7 +501,7 @@
|
||||
color: var(--green79);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs__question.icon--lightgray {
|
||||
[data-theme='dark'] .logs__question.icon--lightgray {
|
||||
color: var(--gray-f3);
|
||||
}
|
||||
|
||||
@ -533,6 +533,6 @@
|
||||
clip: rect(0 0 0 0);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .button-action__icon {
|
||||
[data-theme='dark'] .button-action__icon {
|
||||
color: var(--gray-f3);
|
||||
}
|
||||
|
@ -1,35 +1,36 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import Modal from 'react-modal';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
BLOCK_ACTIONS,
|
||||
MEDIUM_SCREEN_SIZE,
|
||||
} from '../../helpers/constants';
|
||||
import { BLOCK_ACTIONS, MEDIUM_SCREEN_SIZE } from '../../helpers/constants';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
|
||||
import Filters from './Filters';
|
||||
|
||||
import Disabled from './Disabled';
|
||||
import { getFilteringStatus } from '../../actions/filtering';
|
||||
|
||||
import { getClients } from '../../actions';
|
||||
import { getDnsConfig } from '../../actions/dnsConfig';
|
||||
import { getAccessList } from '../../actions/access';
|
||||
import { getAllBlockedServices } from '../../actions/services';
|
||||
import {
|
||||
getLogsConfig,
|
||||
resetFilteredLogs,
|
||||
setFilteredLogs,
|
||||
toggleDetailedLogs,
|
||||
} from '../../actions/queryLogs';
|
||||
import { getLogsConfig, resetFilteredLogs, setFilteredLogs, toggleDetailedLogs } from '../../actions/queryLogs';
|
||||
|
||||
import InfiniteTable from './InfiniteTable';
|
||||
import './Logs.css';
|
||||
import { BUTTON_PREFIX } from './Cells/helpers';
|
||||
import AnonymizerNotification from './AnonymizerNotification';
|
||||
|
||||
const processContent = (data) => Object.entries(data)
|
||||
.map(([key, value]) => {
|
||||
import AnonymizerNotification from './AnonymizerNotification';
|
||||
import { RootState } from '../../initialState';
|
||||
|
||||
const processContent = (data: any, buttonType: string) =>
|
||||
Object.entries(data).map(([key, value]) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
@ -53,12 +54,12 @@ const processContent = (data) => Object.entries(data)
|
||||
<div
|
||||
className={classNames(`key__${key}`, keyClass, {
|
||||
'font-weight-bold': isBoolean && value === true,
|
||||
})}
|
||||
>
|
||||
})}>
|
||||
<Trans>{isButton ? value : key}</Trans>
|
||||
</div>
|
||||
|
||||
<div className={`value__${key} text-pre text-truncate`}>
|
||||
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
|
||||
<Trans>{isTitle || isButton || isBoolean ? '' : value || '—'}</Trans>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -68,20 +69,21 @@ const Logs = () => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
response_status: response_status_url_param,
|
||||
search: search_url_param,
|
||||
} = queryString.parse(history.location.search);
|
||||
const { response_status: response_status_url_param, search: search_url_param } = queryString.parse(
|
||||
history.location.search,
|
||||
);
|
||||
|
||||
const {
|
||||
enabled,
|
||||
processingGetConfig,
|
||||
processingAdditionalLogs,
|
||||
// processingAdditionalLogs,
|
||||
processingGetLogs,
|
||||
anonymize_client_ip: anonymizeClientIp,
|
||||
} = useSelector((state) => state.queryLogs, shallowEqual);
|
||||
const filter = useSelector((state) => state.queryLogs.filter, shallowEqual);
|
||||
const logs = useSelector((state) => state.queryLogs.logs, shallowEqual);
|
||||
} = useSelector((state: RootState) => state.queryLogs, shallowEqual);
|
||||
|
||||
const filter = useSelector((state: RootState) => state.queryLogs.filter, shallowEqual);
|
||||
|
||||
const logs = useSelector((state: RootState) => state.queryLogs.logs, shallowEqual);
|
||||
|
||||
const search = search_url_param || filter?.search || '';
|
||||
const response_status = response_status_url_param || filter?.response_status || '';
|
||||
@ -97,16 +99,18 @@ const Logs = () => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
await dispatch(setFilteredLogs({
|
||||
search,
|
||||
response_status,
|
||||
}));
|
||||
await dispatch(
|
||||
setFilteredLogs({
|
||||
search,
|
||||
response_status,
|
||||
}),
|
||||
);
|
||||
setIsLoading(false);
|
||||
})();
|
||||
}, [response_status, search]);
|
||||
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`);
|
||||
const mediaQueryHandler = (e) => {
|
||||
const mediaQueryHandler = (e: any) => {
|
||||
setIsSmallScreen(e.matches);
|
||||
if (e.matches) {
|
||||
dispatch(toggleDetailedLogs(false));
|
||||
@ -133,11 +137,7 @@ const Logs = () => {
|
||||
dispatch(getClients());
|
||||
dispatch(getAllBlockedServices());
|
||||
try {
|
||||
await Promise.all([
|
||||
dispatch(getLogsConfig()),
|
||||
dispatch(getDnsConfig()),
|
||||
dispatch(getAccessList()),
|
||||
]);
|
||||
await Promise.all([dispatch(getLogsConfig()), dispatch(getDnsConfig()), dispatch(getAccessList())]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
@ -165,70 +165,74 @@ const Logs = () => {
|
||||
if (!history.location.search) {
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
await dispatch(setFilteredLogs());
|
||||
setIsLoading(false);
|
||||
})();
|
||||
}
|
||||
}, [history.location.search]);
|
||||
|
||||
const renderPage = () => <>
|
||||
<Filters
|
||||
const renderPage = () => (
|
||||
<>
|
||||
<Filters
|
||||
filter={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingAdditionalLogs={processingAdditionalLogs}
|
||||
/>
|
||||
<InfiniteTable
|
||||
// processingAdditionalLogs={processingAdditionalLogs}
|
||||
/>
|
||||
|
||||
<InfiniteTable
|
||||
isLoading={isLoading}
|
||||
items={logs}
|
||||
isSmallScreen={isSmallScreen}
|
||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
/>
|
||||
<Modal
|
||||
portalClassName='grid'
|
||||
isOpen={isSmallScreen && isModalOpened}
|
||||
onRequestClose={closeModal}
|
||||
style={{
|
||||
content: {
|
||||
width: 'calc(100% - 32px)',
|
||||
height: 'fit-content',
|
||||
left: '50%',
|
||||
top: 47,
|
||||
padding: '0',
|
||||
maxWidth: '720px',
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="logs__modal-wrap">
|
||||
<svg
|
||||
className="icon icon--24 icon-cross d-block cursor--pointer"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>;
|
||||
/>
|
||||
|
||||
<Modal
|
||||
portalClassName="grid"
|
||||
isOpen={isSmallScreen && isModalOpened}
|
||||
onRequestClose={closeModal}
|
||||
style={{
|
||||
content: {
|
||||
width: 'calc(100% - 32px)',
|
||||
height: 'fit-content',
|
||||
left: '50%',
|
||||
top: 47,
|
||||
padding: '0',
|
||||
maxWidth: '720px',
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
}}>
|
||||
<div className="logs__modal-wrap">
|
||||
<svg className="icon icon--24 icon-cross d-block cursor--pointer" onClick={closeModal}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{enabled && (
|
||||
<>
|
||||
{processingGetConfig && <Loading />}
|
||||
|
||||
{anonymizeClientIp && <AnonymizerNotification />}
|
||||
{!processingGetConfig && renderPage()}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!enabled && !processingGetConfig && <Disabled />}
|
||||
</>
|
||||
);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user