Merge branch 'master' into 7069-fix-blocked-services

This commit is contained in:
Stanislav Chzhen 2024-06-11 13:41:00 +03:00
commit 17a9c14e29
296 changed files with 32125 additions and 32396 deletions

View File

@ -27,6 +27,14 @@ See also the [v0.107.52 GitHub milestone][ms-v0.107.52].
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Changed
- Frontend rewritten in TypeScript.
### Deprecated
- Node 18 support, Node 20 will be required in future releases.
### Fixed ### Fixed
- Panic caused by missing user-specific blocked services object in configuration - Panic caused by missing user-specific blocked services object in configuration

View File

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

View File

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

View File

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

60
client/.eslintrc.json vendored
View File

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

View File

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

10
client/.prettierrc vendored Normal file
View 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
View File

@ -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
View 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
View 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',
],
};
};

View File

@ -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
View File

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

6
client/global.d.ts vendored Normal file
View File

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

View File

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

6
client/jest.config.mjs vendored Normal file
View File

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

39895
client/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

110
client/package.json vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View 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;

View File

@ -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);

View 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);

View File

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

View File

@ -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);

View 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);

View File

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

View File

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

View File

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

View File

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

View File

@ -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);

View 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);

View File

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

View File

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

View File

@ -1,12 +1,23 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import { MODAL_TYPE } from '../../../helpers/constants'; import { MODAL_TYPE } from '../../../helpers/constants';
import Form from './Form'; import Form from './Form';
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 { const {
isModalOpen, isModalOpen,
handleSubmit, handleSubmit,
@ -22,8 +33,7 @@ const Modal = (props) => {
className="Modal__Bootstrap modal-dialog modal-dialog-centered" className="Modal__Bootstrap modal-dialog modal-dialog-centered"
closeTimeoutMS={0} closeTimeoutMS={0}
isOpen={isModalOpen} isOpen={isModalOpen}
onRequestClose={() => toggleRewritesModal()} onRequestClose={() => toggleRewritesModal()}>
>
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h4 className="modal-title"> <h4 className="modal-title">
@ -33,10 +43,12 @@ const Modal = (props) => {
<Trans>rewrite_add</Trans> <Trans>rewrite_add</Trans>
)} )}
</h4> </h4>
<button type="button" className="close" onClick={() => toggleRewritesModal()}> <button type="button" className="close" onClick={() => toggleRewritesModal()}>
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</button> </button>
</div> </div>
<Form <Form
initialValues={{ ...currentRewrite }} initialValues={{ ...currentRewrite }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
@ -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); export default withTranslation()(Modal);

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
&nbsp;&nbsp;
<time>{endTime.hours}:{endTime.minutes}</time>
</div>
);
};
TimePeriod.propTypes = {
startTimeMs: PropTypes.number.isRequired,
endTimeMs: PropTypes.number.isRequired,
};

View File

@ -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>
&nbsp;&nbsp;
<time>
{endTime.hours}:{endTime.minutes}
</time>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View 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;

View File

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

View File

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

View File

@ -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;

View 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;

View File

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

View File

@ -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;

View 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;

View File

@ -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;

View 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;

View File

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

View File

@ -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;

View 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;

View File

@ -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;

View 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;

View File

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

View File

@ -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;

View 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;

View File

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

View File

@ -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);

View 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);

View File

@ -1,12 +1,18 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import Form from './Form'; import Form from './Form';
import { refreshFilteredLogs } from '../../../actions/queryLogs'; import { refreshFilteredLogs } from '../../../actions/queryLogs';
import { addSuccessToast } from '../../../actions/toasts'; import { addSuccessToast } from '../../../actions/toasts';
const Filters = ({ filter, setIsLoading }) => { interface FiltersProps {
filter: object;
processingGetLogs: boolean;
setIsLoading: (...args: unknown[]) => unknown;
}
const Filters = ({ filter, setIsLoading }: FiltersProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -17,32 +23,29 @@ const Filters = ({ filter, setIsLoading }) => {
setIsLoading(false); setIsLoading(false);
}; };
return <div className="page-header page-header--logs"> return (
<h1 className="page-title page-title--large"> <div className="page-header page-header--logs">
{t('query_log')} <h1 className="page-title page-title--large">
<button {t('query_log')}
<button
type="button" type="button"
className="btn btn-icon--green logs__refresh" className="btn btn-icon--green logs__refresh"
title={t('refresh_btn')} title={t('refresh_btn')}
onClick={refreshLogs} onClick={refreshLogs}>
> <svg className="icons icon--24">
<svg className="icons icon--24"> <use xlinkHref="#update" />
<use xlinkHref="#update" /> </svg>
</svg> </button>
</button> </h1>
</h1>
<Form
responseStatusClass="d-sm-block"
initialValues={filter}
setIsLoading={setIsLoading}
/>
</div>;
};
Filters.propTypes = { <Form
filter: PropTypes.object.isRequired, // responseStatusClass="d-sm-block"
processingGetLogs: PropTypes.bool.isRequired, // setIsLoading={setIsLoading}
setIsLoading: PropTypes.func.isRequired, initialValues={filter}
/>
</div>
);
}; };
export default Filters; export default Filters;

View File

@ -1,18 +1,27 @@
import React, { import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef } from 'react';
useCallback,
useEffect,
useRef,
} from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import propTypes from 'prop-types';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import Header from './Cells/Header'; import Header from './Cells/Header';
import { getLogs } from '../../actions/queryLogs'; import { getLogs } from '../../actions/queryLogs';
import Row from './Cells'; import Row from './Cells';
import { isScrolledIntoView } from '../../helpers/helpers'; import { isScrolledIntoView } from '../../helpers/helpers';
import { QUERY_LOGS_PAGE_LIMIT } from '../../helpers/constants'; 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 = ({ const InfiniteTable = ({
isLoading, isLoading,
@ -21,14 +30,15 @@ const InfiniteTable = ({
setDetailedDataCurrent, setDetailedDataCurrent,
setButtonType, setButtonType,
setModalOpened, setModalOpened,
}) => { }: InfiniteTableProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const loader = useRef(null); const loader = useRef(null);
const loadingRef = useRef(null); const loadingRef = useRef(null);
const isEntireLog = useSelector((state) => state.queryLogs.isEntireLog); const isEntireLog = useSelector((state: RootState) => state.queryLogs.isEntireLog);
const processingGetLogs = useSelector((state) => state.queryLogs.processingGetLogs);
const processingGetLogs = useSelector((state: RootState) => state.queryLogs.processingGetLogs);
const loading = isLoading || processingGetLogs; const loading = isLoading || processingGetLogs;
const listener = useCallback(() => { const listener = useCallback(() => {
@ -55,20 +65,23 @@ const InfiniteTable = ({
}; };
}, []); }, []);
const renderRow = (row, idx) => <Row const renderRow = (row: any, idx: any) => (
key={idx} <Row
rowProps={row} key={idx}
isSmallScreen={isSmallScreen} rowProps={row}
setDetailedDataCurrent={setDetailedDataCurrent} isSmallScreen={isSmallScreen}
setButtonType={setButtonType} setDetailedDataCurrent={setDetailedDataCurrent}
setModalOpened={setModalOpened} setButtonType={setButtonType}
/>; setModalOpened={setModalOpened}
/>
);
const isNothingFound = items.length === 0 && !processingGetLogs; const isNothingFound = items.length === 0 && !processingGetLogs;
return ( return (
<div className="logs__table" role="grid"> <div className="logs__table" role="grid">
{loading && <Loading />} {loading && <Loading />}
<Header /> <Header />
{isNothingFound ? ( {isNothingFound ? (
<label className="logs__no-data">{t('nothing_found')}</label> <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; export default InfiniteTable;

View File

@ -24,7 +24,7 @@
--option-border-radius: 4px; --option-border-radius: 4px;
} }
[data-theme="dark"] { [data-theme='dark'] {
--red: rgba(223, 56, 18, 0.25); --red: rgba(223, 56, 18, 0.25);
--green-pale: rgba(103, 178, 121, 0.25); --green-pale: rgba(103, 178, 121, 0.25);
--yellow: rgba(247, 181, 0, 0.2); --yellow: rgba(247, 181, 0, 0.2);
@ -42,11 +42,11 @@
line-height: 1.5rem; line-height: 1.5rem;
} }
[data-theme="dark"] .logs__text a { [data-theme='dark'] .logs__text a {
color: var(--gray-f3); color: var(--gray-f3);
} }
[data-theme="dark"] .logs__text a:hover { [data-theme='dark'] .logs__text a:hover {
color: var(--gray-f3); color: var(--gray-f3);
} }
@ -74,9 +74,9 @@
color: #295a9f; color: #295a9f;
} }
[data-theme="dark"] .logs__text--link, [data-theme='dark'] .logs__text--link,
[data-theme="dark"] .logs__text--link:hover, [data-theme='dark'] .logs__text--link:hover,
[data-theme="dark"] .logs__text--link:focus { [data-theme='dark'] .logs__text--link:focus {
color: var(--gray-f3); color: var(--gray-f3);
} }
@ -90,7 +90,7 @@
border-radius: 4px; border-radius: 4px;
} }
[data-theme=dark] .icon--selected { [data-theme='dark'] .icon--selected {
opacity: 0.75; opacity: 0.75;
} }
@ -131,7 +131,7 @@
} }
.custom-select__arrow--left { .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-position: 5px 9px;
background-size: 22px; background-size: 22px;
} }
@ -150,7 +150,7 @@
background-color: transparent !important; background-color: transparent !important;
} }
[data-theme="dark"] .form-control--transparent option { [data-theme='dark'] .form-control--transparent option {
background-color: var(--card-bgcolor); background-color: var(--card-bgcolor);
} }
@ -351,7 +351,7 @@
overflow: hidden; 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); background: var(--ctrl-dropdown-bgcolor-focus);
} }
@ -387,7 +387,7 @@
background-color: var(--logs__row--blue-bgcolor); 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); color: var(--white);
} }
@ -465,13 +465,13 @@
} }
.logs__whois::after { .logs__whois::after {
content: "|"; content: '|';
padding: 0 5px; padding: 0 5px;
opacity: 0.3; opacity: 0.3;
} }
.logs__whois:last-child::after { .logs__whois:last-child::after {
content: ""; content: '';
} }
.logs__whois-icon.icons { .logs__whois-icon.icons {
@ -501,7 +501,7 @@
color: var(--green79); color: var(--green79);
} }
[data-theme="dark"] .logs__question.icon--lightgray { [data-theme='dark'] .logs__question.icon--lightgray {
color: var(--gray-f3); color: var(--gray-f3);
} }
@ -533,6 +533,6 @@
clip: rect(0 0 0 0); clip: rect(0 0 0 0);
} }
[data-theme="dark"] .button-action__icon { [data-theme='dark'] .button-action__icon {
color: var(--gray-f3); color: var(--gray-f3);
} }

View File

@ -1,35 +1,36 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import Modal from 'react-modal'; import Modal from 'react-modal';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import queryString from 'query-string'; import queryString from 'query-string';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { BLOCK_ACTIONS, MEDIUM_SCREEN_SIZE } from '../../helpers/constants';
BLOCK_ACTIONS,
MEDIUM_SCREEN_SIZE,
} from '../../helpers/constants';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import Filters from './Filters'; import Filters from './Filters';
import Disabled from './Disabled'; import Disabled from './Disabled';
import { getFilteringStatus } from '../../actions/filtering'; import { getFilteringStatus } from '../../actions/filtering';
import { getClients } from '../../actions'; import { getClients } from '../../actions';
import { getDnsConfig } from '../../actions/dnsConfig'; import { getDnsConfig } from '../../actions/dnsConfig';
import { getAccessList } from '../../actions/access'; import { getAccessList } from '../../actions/access';
import { getAllBlockedServices } from '../../actions/services'; import { getAllBlockedServices } from '../../actions/services';
import { import { getLogsConfig, resetFilteredLogs, setFilteredLogs, toggleDetailedLogs } from '../../actions/queryLogs';
getLogsConfig,
resetFilteredLogs,
setFilteredLogs,
toggleDetailedLogs,
} from '../../actions/queryLogs';
import InfiniteTable from './InfiniteTable'; import InfiniteTable from './InfiniteTable';
import './Logs.css'; import './Logs.css';
import { BUTTON_PREFIX } from './Cells/helpers'; import { BUTTON_PREFIX } from './Cells/helpers';
import AnonymizerNotification from './AnonymizerNotification';
const processContent = (data) => Object.entries(data) import AnonymizerNotification from './AnonymizerNotification';
.map(([key, value]) => { import { RootState } from '../../initialState';
const processContent = (data: any, buttonType: string) =>
Object.entries(data).map(([key, value]) => {
if (!value) { if (!value) {
return null; return null;
} }
@ -53,12 +54,12 @@ const processContent = (data) => Object.entries(data)
<div <div
className={classNames(`key__${key}`, keyClass, { className={classNames(`key__${key}`, keyClass, {
'font-weight-bold': isBoolean && value === true, 'font-weight-bold': isBoolean && value === true,
})} })}>
>
<Trans>{isButton ? value : key}</Trans> <Trans>{isButton ? value : key}</Trans>
</div> </div>
<div className={`value__${key} text-pre text-truncate`}> <div className={`value__${key} text-pre text-truncate`}>
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans> <Trans>{isTitle || isButton || isBoolean ? '' : value || '—'}</Trans>
</div> </div>
</div> </div>
); );
@ -68,20 +69,21 @@ const Logs = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const { const { response_status: response_status_url_param, search: search_url_param } = queryString.parse(
response_status: response_status_url_param, history.location.search,
search: search_url_param, );
} = queryString.parse(history.location.search);
const { const {
enabled, enabled,
processingGetConfig, processingGetConfig,
processingAdditionalLogs, // processingAdditionalLogs,
processingGetLogs, processingGetLogs,
anonymize_client_ip: anonymizeClientIp, anonymize_client_ip: anonymizeClientIp,
} = useSelector((state) => state.queryLogs, shallowEqual); } = useSelector((state: RootState) => state.queryLogs, shallowEqual);
const filter = useSelector((state) => state.queryLogs.filter, shallowEqual);
const logs = useSelector((state) => state.queryLogs.logs, 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 search = search_url_param || filter?.search || '';
const response_status = response_status_url_param || filter?.response_status || ''; const response_status = response_status_url_param || filter?.response_status || '';
@ -97,16 +99,18 @@ const Logs = () => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
setIsLoading(true); setIsLoading(true);
await dispatch(setFilteredLogs({ await dispatch(
search, setFilteredLogs({
response_status, search,
})); response_status,
}),
);
setIsLoading(false); setIsLoading(false);
})(); })();
}, [response_status, search]); }, [response_status, search]);
const mediaQuery = window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`); const mediaQuery = window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`);
const mediaQueryHandler = (e) => { const mediaQueryHandler = (e: any) => {
setIsSmallScreen(e.matches); setIsSmallScreen(e.matches);
if (e.matches) { if (e.matches) {
dispatch(toggleDetailedLogs(false)); dispatch(toggleDetailedLogs(false));
@ -133,11 +137,7 @@ const Logs = () => {
dispatch(getClients()); dispatch(getClients());
dispatch(getAllBlockedServices()); dispatch(getAllBlockedServices());
try { try {
await Promise.all([ await Promise.all([dispatch(getLogsConfig()), dispatch(getDnsConfig()), dispatch(getAccessList())]);
dispatch(getLogsConfig()),
dispatch(getDnsConfig()),
dispatch(getAccessList()),
]);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} finally { } finally {
@ -165,70 +165,74 @@ const Logs = () => {
if (!history.location.search) { if (!history.location.search) {
(async () => { (async () => {
setIsLoading(true); setIsLoading(true);
await dispatch(setFilteredLogs()); await dispatch(setFilteredLogs());
setIsLoading(false); setIsLoading(false);
})(); })();
} }
}, [history.location.search]); }, [history.location.search]);
const renderPage = () => <> const renderPage = () => (
<Filters <>
<Filters
filter={{ filter={{
response_status, response_status,
search, search,
}} }}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
processingGetLogs={processingGetLogs} processingGetLogs={processingGetLogs}
processingAdditionalLogs={processingAdditionalLogs} // processingAdditionalLogs={processingAdditionalLogs}
/> />
<InfiniteTable
<InfiniteTable
isLoading={isLoading} isLoading={isLoading}
items={logs} items={logs}
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
setDetailedDataCurrent={setDetailedDataCurrent} setDetailedDataCurrent={setDetailedDataCurrent}
setButtonType={setButtonType} setButtonType={setButtonType}
setModalOpened={setModalOpened} setModalOpened={setModalOpened}
/> />
<Modal
portalClassName='grid' <Modal
isOpen={isSmallScreen && isModalOpened} portalClassName="grid"
onRequestClose={closeModal} isOpen={isSmallScreen && isModalOpened}
style={{ onRequestClose={closeModal}
content: { style={{
width: 'calc(100% - 32px)', content: {
height: 'fit-content', width: 'calc(100% - 32px)',
left: '50%', height: 'fit-content',
top: 47, left: '50%',
padding: '0', top: 47,
maxWidth: '720px', padding: '0',
transform: 'translateX(-50%)', maxWidth: '720px',
}, transform: 'translateX(-50%)',
overlay: { },
backgroundColor: 'rgba(0,0,0,0.5)', overlay: {
}, backgroundColor: 'rgba(0,0,0,0.5)',
}} },
> }}>
<div className="logs__modal-wrap"> <div className="logs__modal-wrap">
<svg <svg className="icon icon--24 icon-cross d-block cursor--pointer" onClick={closeModal}>
className="icon icon--24 icon-cross d-block cursor--pointer" <use xlinkHref="#cross" />
onClick={closeModal} </svg>
>
<use xlinkHref="#cross" /> {processContent(detailedDataCurrent, buttonType)}
</svg> </div>
{processContent(detailedDataCurrent, buttonType)} </Modal>
</div> </>
</Modal> );
</>;
return ( return (
<> <>
{enabled && ( {enabled && (
<> <>
{processingGetConfig && <Loading />} {processingGetConfig && <Loading />}
{anonymizeClientIp && <AnonymizerNotification />} {anonymizeClientIp && <AnonymizerNotification />}
{!processingGetConfig && renderPage()} {!processingGetConfig && renderPage()}
</> </>
)} )}
{!enabled && !processingGetConfig && <Disabled />} {!enabled && !processingGetConfig && <Disabled />}
</> </>
); );

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