Pull request 1717: remove-client2
Merge in DNS/adguard-home from remove-client2 to master Squashed commit of the following: commit 0a0923494b0d1003e4f02f3a5f93d5248f0f2ded Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Fri Jan 20 17:14:29 2023 +0300 all: rm old experimental beta (dir client2/) This project was unfortunately abandoned. v0.108.0 will have a different front-end and new APIs.
This commit is contained in:
parent
6e8033750d
commit
54a141abde
|
@ -12,7 +12,6 @@
|
||||||
/agh-backup/
|
/agh-backup/
|
||||||
/bin/
|
/bin/
|
||||||
/build/*
|
/build/*
|
||||||
/build2/*
|
|
||||||
/data/
|
/data/
|
||||||
/dist/
|
/dist/
|
||||||
/filtering/tests/filtering.TestLotsOfRules*.pprof
|
/filtering/tests/filtering.TestLotsOfRules*.pprof
|
||||||
|
@ -26,4 +25,3 @@ leases.db
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
!/build/gitkeep
|
!/build/gitkeep
|
||||||
!/build2/gitkeep
|
|
||||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -23,6 +23,15 @@ See also the [v0.107.23 GitHub milestone][ms-v0.107.23].
|
||||||
NOTE: Add new changes BELOW THIS COMMENT.
|
NOTE: Add new changes BELOW THIS COMMENT.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
* The “beta frontend” and the corresponding APIs. They never quite worked
|
||||||
|
properly, and the future new version of AdGuard Home API will probably be
|
||||||
|
different.
|
||||||
|
|
||||||
|
Correspondingly, the configuration parameter `beta_bind_port` has been
|
||||||
|
removed as well.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.107.22] - 2023-01-19
|
## [v0.107.22] - 2023-01-19
|
||||||
|
@ -61,7 +70,6 @@ See also the [v0.107.22 GitHub milestone][ms-v0.107.22].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.107.21] - 2022-12-15
|
## [v0.107.21] - 2022-12-15
|
||||||
|
|
||||||
See also the [v0.107.21 GitHub milestone][ms-v0.107.21].
|
See also the [v0.107.21 GitHub milestone][ms-v0.107.21].
|
||||||
|
|
9
Makefile
9
Makefile
|
@ -5,7 +5,6 @@
|
||||||
.POSIX:
|
.POSIX:
|
||||||
|
|
||||||
CHANNEL = development
|
CHANNEL = development
|
||||||
CLIENT_BETA_DIR = client2
|
|
||||||
CLIENT_DIR = client
|
CLIENT_DIR = client
|
||||||
COMMIT = $$( git rev-parse --short HEAD )
|
COMMIT = $$( git rev-parse --short HEAD )
|
||||||
DIST_DIR = dist
|
DIST_DIR = dist
|
||||||
|
@ -29,10 +28,6 @@ SIGN = 1
|
||||||
VERBOSE = 0
|
VERBOSE = 0
|
||||||
VERSION = v0.0.0
|
VERSION = v0.0.0
|
||||||
YARN = yarn
|
YARN = yarn
|
||||||
YARN_FLAGS = --cwd $(CLIENT_BETA_DIR)
|
|
||||||
YARN_INSTALL_FLAGS = $(YARN_FLAGS) --network-timeout 120000 --silent\
|
|
||||||
--ignore-engines --ignore-optional --ignore-platform\
|
|
||||||
--ignore-scripts
|
|
||||||
|
|
||||||
NEXTAPI = 0
|
NEXTAPI = 0
|
||||||
|
|
||||||
|
@ -93,17 +88,13 @@ init: ; git config core.hooksPath ./scripts/hooks
|
||||||
|
|
||||||
js-build:
|
js-build:
|
||||||
$(NPM) $(NPM_FLAGS) run build-prod
|
$(NPM) $(NPM_FLAGS) run build-prod
|
||||||
$(YARN) $(YARN_FLAGS) build
|
|
||||||
js-deps:
|
js-deps:
|
||||||
$(NPM) $(NPM_INSTALL_FLAGS) ci
|
$(NPM) $(NPM_INSTALL_FLAGS) ci
|
||||||
$(YARN) $(YARN_INSTALL_FLAGS) install
|
|
||||||
|
|
||||||
# TODO(a.garipov): Remove the legacy client tasks support once the new
|
# TODO(a.garipov): Remove the legacy client tasks support once the new
|
||||||
# client is done and the old one is removed.
|
# client is done and the old one is removed.
|
||||||
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
||||||
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
||||||
js-beta-lint: ; $(YARN) $(YARN_FLAGS) lint
|
|
||||||
js-beta-test: ; # TODO(v.abdulmyanov): Add tests for the new client.
|
|
||||||
|
|
||||||
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
||||||
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
|
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Keep this file non-hidden for Go's embedding to work.
|
|
|
@ -1,6 +0,0 @@
|
||||||
scripts
|
|
||||||
node_modules
|
|
||||||
postcss.config.js
|
|
||||||
src/lib/entities
|
|
||||||
src/lib/apis
|
|
||||||
openApi
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"extends": [
|
|
||||||
"./scripts/lint/dev.js"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
declare module '*.pcss' {
|
|
||||||
const content: {[className: string]: string};
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
declare module '*.css' {
|
|
||||||
const content: {[className: string]: string};
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
declare module '*.png'
|
|
||||||
declare module '*.jpg'
|
|
||||||
declare let AUTH_TOKEN: string;
|
|
||||||
declare let MAIN_TOKEN: string | undefined;
|
|
||||||
declare let NO_CAPTCHA: boolean | undefined;
|
|
||||||
declare module 'dygraphs';
|
|
||||||
declare module '@novnc/novnc/core/rfb';
|
|
||||||
// cp - CloudPayments script
|
|
||||||
declare let cp: any;
|
|
||||||
declare const DEV: any;
|
|
|
@ -1,89 +0,0 @@
|
||||||
{
|
|
||||||
"author": "Performix",
|
|
||||||
"private": true,
|
|
||||||
"name": "adguard-home",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack --config ./scripts/webpack/webpack.config.prod.js",
|
|
||||||
"start": "webpack serve --config ./scripts/webpack/webpack.config.dev.js",
|
|
||||||
"generate": "rm -rf ./src/lib/entities ./src/lib/apis && ts-node --compiler-options '{ \"module\": \"CommonJS\" }' ./scripts/generator/index.ts",
|
|
||||||
"translations:check": "ts-node --compiler-options '{ \"module\": \"CommonJS\" }' ./scripts/plugins/checkTranslations.ts",
|
|
||||||
"lint": "eslint -c ./scripts/lint/prod.js --ext .tsx --ext .ts ./",
|
|
||||||
"go:build": "cd .. && make REBUILD_CLIENT=0 build",
|
|
||||||
"go:run": "sudo ../AdguardHome"
|
|
||||||
},
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@adguard/translate": "^0.2.0",
|
|
||||||
"@ant-design/icons": "^4.4.0",
|
|
||||||
"@sentry/react": "^5.27.0",
|
|
||||||
"antd": "^4.7.2",
|
|
||||||
"classnames": "^2.2.6",
|
|
||||||
"dayjs": "^1.9.3",
|
|
||||||
"formik": "^2.2.0",
|
|
||||||
"mobx": "^6.0.1",
|
|
||||||
"mobx-react-lite": "^3.0.1",
|
|
||||||
"qs": "^6.9.4",
|
|
||||||
"react": "^17.0.0",
|
|
||||||
"react-dom": "^17.0.0",
|
|
||||||
"react-router-dom": "^5.2.0",
|
|
||||||
"recharts": "^2.0.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/classnames": "^2.2.10",
|
|
||||||
"@types/qs": "^6.9.5",
|
|
||||||
"@types/react": "^16.9.53",
|
|
||||||
"@types/react-dom": "^16.9.8",
|
|
||||||
"@types/react-redux": "^7.1.9",
|
|
||||||
"@types/react-router-dom": "^5.1.6",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^4.5.0",
|
|
||||||
"@typescript-eslint/parser": "^4.5.0",
|
|
||||||
"antd-dayjs-webpack-plugin": "^1.0.1",
|
|
||||||
"autoprefixer": "^10.0.1",
|
|
||||||
"connect-history-api-fallback": "^1.6.0",
|
|
||||||
"copy-webpack-plugin": "^6.2.1",
|
|
||||||
"css-loader": "^5.0.0",
|
|
||||||
"eslint": "^7.11.0",
|
|
||||||
"eslint-config-airbnb-base": "^14.2.0",
|
|
||||||
"eslint-config-airbnb-typescript": "^12.0.0",
|
|
||||||
"eslint-import-resolver-typescript": "^2.3.0",
|
|
||||||
"eslint-loader": "^4.0.2",
|
|
||||||
"eslint-plugin-import": "^2.22.1",
|
|
||||||
"eslint-plugin-react": "^7.21.5",
|
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
|
||||||
"file-loader": "^6.1.1",
|
|
||||||
"html-webpack-plugin": "^4.5.0",
|
|
||||||
"http-proxy-middleware": "^1.0.6",
|
|
||||||
"less": "^3.12.2",
|
|
||||||
"less-loader": "^5.0.0",
|
|
||||||
"mini-css-extract-plugin": "^1.1.1",
|
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
|
||||||
"postcss": "^8.1.2",
|
|
||||||
"postcss-calc": "^7.0.5",
|
|
||||||
"postcss-css-variables": "^0.17.0",
|
|
||||||
"postcss-custom-media": "^7.0.8",
|
|
||||||
"postcss-import": "^13.0.0",
|
|
||||||
"postcss-inline-svg": "^4.1.0",
|
|
||||||
"postcss-loader": "^4.0.4",
|
|
||||||
"postcss-mixins": "^7.0.1",
|
|
||||||
"postcss-modules": "^3.2.2",
|
|
||||||
"postcss-nested": "^5.0.1",
|
|
||||||
"postcss-preset-env": "^6.7.0",
|
|
||||||
"postcss-reporter": "^7.0.1",
|
|
||||||
"postcss-variables": "^1.1.1",
|
|
||||||
"style-loader": "^2.0.0",
|
|
||||||
"stylelint": "^13.7.2",
|
|
||||||
"stylelint-webpack-plugin": "^2.1.1",
|
|
||||||
"terser-webpack-plugin": "^5.0.0",
|
|
||||||
"ts-loader": "^8.0.6",
|
|
||||||
"ts-morph": "^8.1.2",
|
|
||||||
"ts-node": "^9.0.0",
|
|
||||||
"typescript": "^4.0.3",
|
|
||||||
"url-loader": "^4.1.1",
|
|
||||||
"webpack": "^5.10.0",
|
|
||||||
"webpack-cli": "^4.2.0",
|
|
||||||
"webpack-dev-server": "^3.11.0",
|
|
||||||
"webpack-merge": "^5.2.0",
|
|
||||||
"yaml": "^1.10.0"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
plugins: [
|
|
||||||
['postcss-import', {}],
|
|
||||||
['postcss-nested', {}],
|
|
||||||
['postcss-custom-media', {}],
|
|
||||||
['postcss-variables', {}],
|
|
||||||
['postcss-calc', {}],
|
|
||||||
['postcss-mixins', {}],
|
|
||||||
['postcss-preset-env', { stage: 3, features: { 'nesting-rules': true } }],
|
|
||||||
['postcss-reporter', { clearMessages: true }],
|
|
||||||
['postcss-inline-svg', {
|
|
||||||
paths: ['frontend/icons', 'vendor/adguard/utils-bundle/src/Resources/frontend/icons'],
|
|
||||||
svgo: { plugins: [{ cleanupAttrs: true }] }
|
|
||||||
}],
|
|
||||||
['autoprefixer'],
|
|
||||||
]
|
|
||||||
};
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16pt" height="16pt"
|
|
||||||
viewBox="0 0 16 16" version="1.1">
|
|
||||||
<g id="surface1">
|
|
||||||
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;"
|
|
||||||
d="M 8 0 C 10.5 0 13.515625 0.574219 16 1.835938 L 15.996094 2.542969 C 15.957031 5.605469 15.410156 11.71875 8 16 C 0.5 11.667969 0.03125 5.460938 0.00390625 2.433594 L 0 1.835938 C 2.484375 0.574219 5.5 0 8 0 Z M 11.769531 4.203125 L 11.761719 4.203125 L 7.890625 8.160156 L 6.433594 6.4375 C 5.738281 5.644531 4.792969 6.25 4.570312 6.40625 L 7.929688 10.285156 L 12.570312 4.136719 C 12.230469 3.867188 11.933594 4.054688 11.769531 4.203125 Z M 11.769531 4.203125 "/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 801 B |
|
@ -1,23 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
|
|
||||||
<meta name="theme-color" content="#000000">
|
|
||||||
<meta name="google" content="notranslate">
|
|
||||||
<meta http-equiv="x-dns-prefetch-control" content="off">
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon-180x180.png" />
|
|
||||||
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
|
|
||||||
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
|
|
||||||
<title>AdGuard Home</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
You need to enable JavaScript to run this app.
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,22 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
|
|
||||||
<meta name="theme-color" content="#000000">
|
|
||||||
<meta name="google" content="notranslate">
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon-180x180.png" />
|
|
||||||
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
|
|
||||||
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
|
|
||||||
<title>Setup AdGuard Home</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
You need to enable JavaScript to run this app.
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,22 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
|
|
||||||
<meta name="theme-color" content="#000000">
|
|
||||||
<meta name="google" content="notranslate">
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon-180x180.png" />
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
||||||
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
|
|
||||||
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
|
|
||||||
<title>Login</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
You need to enable JavaScript to run this app.
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,12 +0,0 @@
|
||||||
export const OPEN_API_PATH = '../openapi/openapi.yaml';
|
|
||||||
export const ENT_DIR = './src/lib/entities';
|
|
||||||
export const API_DIR = './src/lib/apis';
|
|
||||||
export const LOCALE_FOLDER_PATH = './src/lib/intl/__locales';
|
|
||||||
export const TRANSLATOR_CLASS_NAME = 'Translator';
|
|
||||||
export const USE_INTL_NAME = 'useIntl';
|
|
||||||
|
|
||||||
export const trimQuotes = (str: string) => {
|
|
||||||
return str.replace(/\'|\"/g, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GENERATOR_ENTITY_ALLIAS = 'Entities/';
|
|
|
@ -1,18 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as YAML from 'yaml';
|
|
||||||
import { OPEN_API_PATH } from '../consts';
|
|
||||||
|
|
||||||
import EntitiesGenerator from './src/generateEntities';
|
|
||||||
import ApisGenerator from './src/generateApis';
|
|
||||||
|
|
||||||
|
|
||||||
const generateApi = (openApi: Record<string, any>) => {
|
|
||||||
const ent = new EntitiesGenerator(openApi);
|
|
||||||
ent.save();
|
|
||||||
|
|
||||||
const api = new ApisGenerator(openApi);
|
|
||||||
api.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
const openApiFile = fs.readFileSync(OPEN_API_PATH, 'utf8');
|
|
||||||
generateApi(YAML.parse(openApiFile));
|
|
|
@ -1,317 +0,0 @@
|
||||||
/* eslint-disable no-template-curly-in-string */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { stringify } from 'qs';
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
import * as morph from 'ts-morph';
|
|
||||||
|
|
||||||
import {
|
|
||||||
API_DIR as API_DIR_CONST,
|
|
||||||
GENERATOR_ENTITY_ALLIAS,
|
|
||||||
} from '../../consts';
|
|
||||||
import { toCamel, capitalize, schemaParamParser } from './utils';
|
|
||||||
|
|
||||||
|
|
||||||
const API_DIR = path.resolve(API_DIR_CONST);
|
|
||||||
if (!fs.existsSync(API_DIR)) {
|
|
||||||
fs.mkdirSync(API_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Project, QuoteKind } = morph;
|
|
||||||
|
|
||||||
|
|
||||||
class ApiGenerator {
|
|
||||||
project = new Project({
|
|
||||||
tsConfigFilePath: './tsconfig.json',
|
|
||||||
addFilesFromTsConfig: false,
|
|
||||||
manipulationSettings: {
|
|
||||||
quoteKind: QuoteKind.Single,
|
|
||||||
usePrefixAndSuffixTextForRename: false,
|
|
||||||
useTrailingCommas: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
openapi: Record<string, any>;
|
|
||||||
|
|
||||||
serverUrl: string;
|
|
||||||
|
|
||||||
paths: any;
|
|
||||||
|
|
||||||
/* interface Controllers {
|
|
||||||
[controller: string]: {
|
|
||||||
[operationId: string]: { parameters - from opneApi, responses - from opneApi, method }
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
controllers: Record<string, any> = {};
|
|
||||||
|
|
||||||
apis: morph.SourceFile[] = [];
|
|
||||||
|
|
||||||
constructor(openapi: Record<string, any>) {
|
|
||||||
this.openapi = openapi;
|
|
||||||
this.paths = openapi.paths;
|
|
||||||
this.serverUrl = openapi.servers[0].url;
|
|
||||||
|
|
||||||
Object.keys(this.paths).forEach((pathKey) => {
|
|
||||||
Object.keys(this.paths[pathKey]).forEach((method) => {
|
|
||||||
const {
|
|
||||||
tags, operationId, parameters, responses, requestBody, security,
|
|
||||||
} = this.paths[pathKey][method];
|
|
||||||
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', ''));
|
|
||||||
|
|
||||||
if (this.controllers[controller]) {
|
|
||||||
this.controllers[controller][operationId] = {
|
|
||||||
parameters,
|
|
||||||
responses,
|
|
||||||
method,
|
|
||||||
requestBody,
|
|
||||||
security,
|
|
||||||
pathKey: pathKey.replace(/{/g, '${'),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.controllers[controller] = { [operationId]: {
|
|
||||||
parameters,
|
|
||||||
responses,
|
|
||||||
method,
|
|
||||||
requestBody,
|
|
||||||
security,
|
|
||||||
pathKey: pathKey.replace(/{/g, '${'),
|
|
||||||
} };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.generateApiFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateApiFiles = () => {
|
|
||||||
Object.keys(this.controllers).forEach(this.generateApiFile);
|
|
||||||
};
|
|
||||||
|
|
||||||
generateApiFile = (cName: string) => {
|
|
||||||
const apiFile = this.project.createSourceFile(`${API_DIR}/${cName}.ts`);
|
|
||||||
apiFile.addStatements([
|
|
||||||
'// This file was autogenerated. Please do not change.',
|
|
||||||
'// All changes will be overwrited on commit.',
|
|
||||||
'',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// const schemaProperties = schemas[schemaName].properties;
|
|
||||||
const importEntities: any[] = [];
|
|
||||||
|
|
||||||
// add api class to file
|
|
||||||
const apiClass = apiFile.addClass({
|
|
||||||
name: `${capitalize(cName)}Api`,
|
|
||||||
isDefaultExport: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// get operations of controller
|
|
||||||
const controllerOperations = this.controllers[cName];
|
|
||||||
const operationList = Object.keys(controllerOperations).sort();
|
|
||||||
// for each operation add fetcher
|
|
||||||
operationList.forEach((operation) => {
|
|
||||||
const {
|
|
||||||
requestBody, responses, parameters, method, pathKey, security,
|
|
||||||
} = controllerOperations[operation];
|
|
||||||
|
|
||||||
const queryParams: any[] = []; // { name, type }
|
|
||||||
const bodyParam: any[] = []; // { name, type }
|
|
||||||
|
|
||||||
let hasResponseBodyType: /* boolean | ReturnType<schemaParamParser> */ false | [string, boolean, boolean, boolean, boolean] = false;
|
|
||||||
let contentType = '';
|
|
||||||
if (parameters) {
|
|
||||||
parameters.forEach((p: any) => {
|
|
||||||
const [
|
|
||||||
pType, isArray, isClass, isImport,
|
|
||||||
] = schemaParamParser(p.schema, this.openapi);
|
|
||||||
|
|
||||||
if (isImport) {
|
|
||||||
importEntities.push({ type: pType, isClass });
|
|
||||||
}
|
|
||||||
if (p.in === 'query') {
|
|
||||||
queryParams.push({
|
|
||||||
name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (queryParams.length > 0) {
|
|
||||||
const imp = apiFile.getImportDeclaration((i) => {
|
|
||||||
return i.getModuleSpecifierValue() === 'qs';
|
|
||||||
}); if (!imp) {
|
|
||||||
apiFile.addImportDeclaration({
|
|
||||||
moduleSpecifier: 'qs',
|
|
||||||
defaultImport: 'qs',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (requestBody) {
|
|
||||||
let content = requestBody.content;
|
|
||||||
const { $ref }: { $ref: string } = requestBody;
|
|
||||||
|
|
||||||
if (!content && $ref) {
|
|
||||||
const name = $ref.split('/').pop() as string;
|
|
||||||
content = this.openapi.components.requestBodies[name].content;
|
|
||||||
}
|
|
||||||
|
|
||||||
[contentType] = Object.keys(content);
|
|
||||||
const data = content[contentType];
|
|
||||||
|
|
||||||
const [
|
|
||||||
pType, isArray, isClass, isImport,
|
|
||||||
] = schemaParamParser(data.schema, this.openapi);
|
|
||||||
|
|
||||||
if (isImport) {
|
|
||||||
importEntities.push({ type: pType, isClass });
|
|
||||||
bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType });
|
|
||||||
} else {
|
|
||||||
bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` });
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (responses['200']) {
|
|
||||||
const { content, headers } = responses['200'];
|
|
||||||
if (content && (content['*/*'] || content['application/json'])) {
|
|
||||||
const { schema, examples } = content['*/*'] || content['application/json'];
|
|
||||||
|
|
||||||
if (!schema) {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const propType = schemaParamParser(schema, this.openapi);
|
|
||||||
const [pType, , isClass, isImport] = propType;
|
|
||||||
|
|
||||||
if (isImport) {
|
|
||||||
importEntities.push({ type: pType, isClass });
|
|
||||||
}
|
|
||||||
hasResponseBodyType = propType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let returnType = '';
|
|
||||||
if (hasResponseBodyType) {
|
|
||||||
const [pType, isArray, isClass] = hasResponseBodyType as any;
|
|
||||||
let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
|
||||||
returnType = data;
|
|
||||||
} else {
|
|
||||||
returnType = 'Promise<number';
|
|
||||||
}
|
|
||||||
const shouldValidate = bodyParam.filter(b => b.isClass);
|
|
||||||
if (shouldValidate.length > 0) {
|
|
||||||
returnType += ' | string[]';
|
|
||||||
}
|
|
||||||
// append Error to default type return;
|
|
||||||
returnType += ' | Error>';
|
|
||||||
|
|
||||||
const fetcher = apiClass.addMethod({
|
|
||||||
isAsync: true,
|
|
||||||
isStatic: true,
|
|
||||||
name: operation,
|
|
||||||
returnType,
|
|
||||||
});
|
|
||||||
const params = [...queryParams, ...bodyParam].sort((a, b) => (Number(!!a.hasQuestionToken) - Number(!!b.hasQuestionToken)));
|
|
||||||
fetcher.addParameters(params);
|
|
||||||
|
|
||||||
fetcher.setBodyText((w) => {
|
|
||||||
// Add data to URLSearchParams
|
|
||||||
if (contentType === 'text/plain') {
|
|
||||||
bodyParam.forEach((b) => {
|
|
||||||
w.writeLine(`const params = String(${b.name});`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (shouldValidate.length > 0) {
|
|
||||||
w.writeLine(`const haveError: string[] = [];`);
|
|
||||||
shouldValidate.forEach((b) => {
|
|
||||||
w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`);
|
|
||||||
w.writeLine(`haveError.push(...${b.name}Valid.validate());`);
|
|
||||||
});
|
|
||||||
w.writeLine(`if (haveError.length > 0) {`);
|
|
||||||
w.writeLine(` return Promise.resolve(haveError);`)
|
|
||||||
w.writeLine(`}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Switch return of fetch in case on queryParams
|
|
||||||
if (queryParams.length > 0) {
|
|
||||||
w.writeLine('const queryParams = {');
|
|
||||||
queryParams.forEach((q) => {
|
|
||||||
w.writeLine(` ${q.name}: ${q.name},`);
|
|
||||||
});
|
|
||||||
w.writeLine('}');
|
|
||||||
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}?\${qs.stringify(queryParams, { arrayFormat: 'comma' })}\`, {`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}\`, {`);
|
|
||||||
}
|
|
||||||
// Add method
|
|
||||||
w.writeLine(` method: '${method.toUpperCase()}',`);
|
|
||||||
|
|
||||||
// add Fetch options
|
|
||||||
if (contentType && contentType !== 'multipart/form-data') {
|
|
||||||
w.writeLine(' headers: {');
|
|
||||||
w.writeLine(` 'Content-Type': '${contentType}',`);
|
|
||||||
w.writeLine(' },');
|
|
||||||
}
|
|
||||||
if (contentType) {
|
|
||||||
switch (contentType) {
|
|
||||||
case 'text/plain':
|
|
||||||
w.writeLine(' body: params,');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle response
|
|
||||||
if (hasResponseBodyType) {
|
|
||||||
w.writeLine('}).then(async (res) => {');
|
|
||||||
w.writeLine(' if (res.status === 200) {');
|
|
||||||
w.writeLine(' return res.json();');
|
|
||||||
} else {
|
|
||||||
w.writeLine('}).then(async (res) => {');
|
|
||||||
w.writeLine(' if (res.status === 200) {');
|
|
||||||
w.writeLine(' return res.status;');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Error
|
|
||||||
w.writeLine(' } else {');
|
|
||||||
w.writeLine(' return new Error(String(res.status));');
|
|
||||||
w.writeLine(' }');
|
|
||||||
w.writeLine('})');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const imports: any[] = [];
|
|
||||||
const types: string[] = [];
|
|
||||||
importEntities.forEach((i) => {
|
|
||||||
const { type } = i;
|
|
||||||
if (!types.includes(type)) {
|
|
||||||
imports.push(i);
|
|
||||||
types.push(type);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
|
||||||
const { type: pType, isClass } = ie;
|
|
||||||
if (isClass) {
|
|
||||||
apiFile.addImportDeclaration({
|
|
||||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
|
||||||
defaultImport: pType,
|
|
||||||
namedImports: [`I${pType}`],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
apiFile.addImportDeclaration({
|
|
||||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
|
||||||
namedImports: [pType],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.apis.push(apiFile);
|
|
||||||
};
|
|
||||||
|
|
||||||
save = () => {
|
|
||||||
this.apis.forEach(async (e) => {
|
|
||||||
await e.saveSync();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default ApiGenerator;
|
|
|
@ -1,603 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
import * as morph from 'ts-morph';
|
|
||||||
|
|
||||||
import { ENT_DIR } from '../../consts';
|
|
||||||
import { TYPES, toCamel, schemaParamParser, uncapitalize } from './utils';
|
|
||||||
|
|
||||||
const { Project, QuoteKind } = morph;
|
|
||||||
|
|
||||||
|
|
||||||
const EntDir = path.resolve(ENT_DIR);
|
|
||||||
if (!fs.existsSync(EntDir)) {
|
|
||||||
fs.mkdirSync(EntDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
class EntitiesGenerator {
|
|
||||||
project = new Project({
|
|
||||||
tsConfigFilePath: './tsconfig.json',
|
|
||||||
addFilesFromTsConfig: false,
|
|
||||||
manipulationSettings: {
|
|
||||||
quoteKind: QuoteKind.Single,
|
|
||||||
usePrefixAndSuffixTextForRename: false,
|
|
||||||
useTrailingCommas: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
openapi: Record<string, any>;
|
|
||||||
|
|
||||||
schemas: Record<string, any>;
|
|
||||||
|
|
||||||
schemaNames: string[];
|
|
||||||
|
|
||||||
entities: morph.SourceFile[] = [];
|
|
||||||
|
|
||||||
constructor(openapi: Record<string, any>) {
|
|
||||||
this.openapi = openapi;
|
|
||||||
this.schemas = openapi.components.schemas;
|
|
||||||
this.schemaNames = Object.keys(this.schemas);
|
|
||||||
this.generateEntities();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateEntities = () => {
|
|
||||||
this.schemaNames.forEach(this.generateEntity);
|
|
||||||
};
|
|
||||||
|
|
||||||
generateEntity = (sName: string) => {
|
|
||||||
const { properties, type, oneOf } = this.schemas[sName];
|
|
||||||
const notAClass = !properties && TYPES[type as keyof typeof TYPES];
|
|
||||||
|
|
||||||
if (oneOf) {
|
|
||||||
this.generateOneOf(sName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notAClass) {
|
|
||||||
this.generateEnum(sName);
|
|
||||||
} else {
|
|
||||||
this.generateClass(sName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
generateEnum = (sName: string) => {
|
|
||||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
|
||||||
entityFile.addStatements([
|
|
||||||
'// This file was autogenerated. Please do not change.',
|
|
||||||
'// All changes will be overwrited on commit.',
|
|
||||||
'',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { enum: enumMembers } = this.schemas[sName];
|
|
||||||
entityFile.addEnum({
|
|
||||||
name: sName,
|
|
||||||
members: enumMembers.map((e: string) => ({ name: e.toUpperCase(), value: e })),
|
|
||||||
isExported: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.entities.push(entityFile);
|
|
||||||
};
|
|
||||||
|
|
||||||
generateOneOf = (sName: string) => {
|
|
||||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
|
||||||
entityFile.addStatements([
|
|
||||||
'// This file was autogenerated. Please do not change.',
|
|
||||||
'// All changes will be overwrited on commit.',
|
|
||||||
'',
|
|
||||||
]);
|
|
||||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
|
||||||
const entities = this.schemas[sName].oneOf.map((elem: any) => {
|
|
||||||
const [
|
|
||||||
pType, isArray, isClass, isImport,
|
|
||||||
] = schemaParamParser(elem, this.openapi);
|
|
||||||
importEntities.push({ type: pType, isClass });
|
|
||||||
return { type: pType, isArray };
|
|
||||||
});
|
|
||||||
entityFile.addTypeAlias({
|
|
||||||
name: sName,
|
|
||||||
isExported: true,
|
|
||||||
type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '),
|
|
||||||
})
|
|
||||||
|
|
||||||
// add import
|
|
||||||
importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
|
||||||
const { type: pType, isClass } = ie;
|
|
||||||
if (isClass) {
|
|
||||||
entityFile.addImportDeclaration({
|
|
||||||
moduleSpecifier: `./${pType}`,
|
|
||||||
namedImports: [`I${pType}`],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
entityFile.addImportDeclaration({
|
|
||||||
moduleSpecifier: `./${pType}`,
|
|
||||||
namedImports: [pType],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.entities.push(entityFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateClass = (sName: string) => {
|
|
||||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
|
||||||
entityFile.addStatements([
|
|
||||||
'// This file was autogenerated. Please do not change.',
|
|
||||||
'// All changes will be overwrited on commit.',
|
|
||||||
'',
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
const { properties: sProps, required, $ref, additionalProperties } = this.schemas[sName];
|
|
||||||
if ($ref) {
|
|
||||||
const temp = $ref.split('/');
|
|
||||||
const importSchemaName = `${temp[temp.length - 1]}`;
|
|
||||||
entityFile.addImportDeclaration({
|
|
||||||
defaultImport: importSchemaName,
|
|
||||||
moduleSpecifier: `./${importSchemaName}`,
|
|
||||||
namedImports: [`I${importSchemaName}`],
|
|
||||||
});
|
|
||||||
|
|
||||||
entityFile.addTypeAlias({
|
|
||||||
name: `I${sName}`,
|
|
||||||
type: `I${importSchemaName}`,
|
|
||||||
isExported: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
entityFile.addStatements(`export default ${importSchemaName};`);
|
|
||||||
this.entities.push(entityFile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
|
||||||
const entityInterface = entityFile.addInterface({
|
|
||||||
name: `I${sName}`,
|
|
||||||
isExported: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedSProps = Object.keys(sProps || {}).sort();
|
|
||||||
const additionalPropsOnly = additionalProperties && sortedSProps.length === 0;
|
|
||||||
|
|
||||||
// add server response interface to entityFile
|
|
||||||
sortedSProps.forEach((sPropName) => {
|
|
||||||
const [
|
|
||||||
pType, isArray, isClass, isImport, isAdditional
|
|
||||||
] = schemaParamParser(sProps[sPropName], this.openapi);
|
|
||||||
|
|
||||||
if (isImport) {
|
|
||||||
importEntities.push({ type: pType, isClass });
|
|
||||||
}
|
|
||||||
const propertyType = isAdditional
|
|
||||||
? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
|
|
||||||
: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
|
||||||
entityInterface.addProperty({
|
|
||||||
name: sPropName,
|
|
||||||
type: propertyType,
|
|
||||||
hasQuestionToken: !(
|
|
||||||
(required && required.includes(sPropName)) || sProps[sPropName].required
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (additionalProperties) {
|
|
||||||
const [
|
|
||||||
pType, isArray, isClass, isImport, isAdditional
|
|
||||||
] = schemaParamParser(additionalProperties, this.openapi);
|
|
||||||
|
|
||||||
if (isImport) {
|
|
||||||
importEntities.push({ type: pType, isClass });
|
|
||||||
}
|
|
||||||
const type = isAdditional
|
|
||||||
? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
|
|
||||||
: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
|
||||||
entityInterface.addIndexSignature({
|
|
||||||
keyName: 'key',
|
|
||||||
keyType: 'string',
|
|
||||||
returnType: additionalPropsOnly ? type : `${type} | undefined`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// add import
|
|
||||||
const imports: { type: string, isClass: boolean }[] = [];
|
|
||||||
const types: string[] = [];
|
|
||||||
importEntities.forEach((i) => {
|
|
||||||
const { type } = i;
|
|
||||||
if (!types.includes(type)) {
|
|
||||||
imports.push(i);
|
|
||||||
types.push(type);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
imports.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
|
||||||
const { type: pType, isClass } = ie;
|
|
||||||
if (isClass) {
|
|
||||||
entityFile.addImportDeclaration({
|
|
||||||
defaultImport: pType,
|
|
||||||
moduleSpecifier: `./${pType}`,
|
|
||||||
namedImports: [`I${pType}`],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
entityFile.addImportDeclaration({
|
|
||||||
moduleSpecifier: `./${pType}`,
|
|
||||||
namedImports: [pType],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const entityClass = entityFile.addClass({
|
|
||||||
name: sName,
|
|
||||||
isDefaultExport: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// addProperties to class;
|
|
||||||
sortedSProps.forEach((sPropName) => {
|
|
||||||
const [pType, isArray, isClass, isImport, isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
|
||||||
|
|
||||||
const isRequred = (required && required.includes(sPropName))
|
|
||||||
|| sProps[sPropName].required;
|
|
||||||
|
|
||||||
const propertyType = isAdditional
|
|
||||||
? `{ [key: string]: ${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'} }`
|
|
||||||
: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`;
|
|
||||||
|
|
||||||
entityClass.addProperty({
|
|
||||||
name: `_${sPropName}`,
|
|
||||||
isReadonly: true,
|
|
||||||
type: propertyType,
|
|
||||||
});
|
|
||||||
const getter = entityClass.addGetAccessor({
|
|
||||||
name: toCamel(sPropName),
|
|
||||||
returnType: propertyType,
|
|
||||||
statements: [`return this._${sPropName};`],
|
|
||||||
});
|
|
||||||
const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
|
||||||
if (description || example) {
|
|
||||||
getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`);
|
|
||||||
}
|
|
||||||
if (minItems) {
|
|
||||||
entityClass.addGetAccessor({
|
|
||||||
isStatic: true,
|
|
||||||
name: `${toCamel(sPropName)}MinItems`,
|
|
||||||
statements: [`return ${minItems};`],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (maxItems) {
|
|
||||||
entityClass.addGetAccessor({
|
|
||||||
isStatic: true,
|
|
||||||
name: `${toCamel(sPropName)}MaxItems`,
|
|
||||||
statements: [`return ${maxItems};`],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (typeof minLength === 'number') {
|
|
||||||
entityClass.addGetAccessor({
|
|
||||||
isStatic: true,
|
|
||||||
name: `${toCamel(sPropName)}MinLength`,
|
|
||||||
statements: [`return ${minLength};`],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (maxLength) {
|
|
||||||
entityClass.addGetAccessor({
|
|
||||||
isStatic: true,
|
|
||||||
name: `${toCamel(sPropName)}MaxLength`,
|
|
||||||
statements: [`return ${maxLength};`],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (typeof minimum === 'number') {
|
|
||||||
entityClass.addGetAccessor({
|
|
||||||
isStatic: true,
|
|
||||||
name: `${toCamel(sPropName)}MinValue`,
|
|
||||||
statements: [`return ${minimum};`],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (maximum) {
|
|
||||||
entityClass.addGetAccessor({
|
|
||||||
isStatic: true,
|
|
||||||
name: `${toCamel(sPropName)}MaxValue`,
|
|
||||||
statements: [`return ${maximum};`],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(isArray && isClass) && !isClass) {
|
|
||||||
const isEnum = !isClass && isImport;
|
|
||||||
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
|
|
||||||
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
|
||||||
const haveValidationFields = maxLength || typeof minLength === 'number' || maximum || typeof minimum === 'number';
|
|
||||||
if (isRequired || haveValidationFields) {
|
|
||||||
const prop = toCamel(sPropName);
|
|
||||||
const validateField = entityClass.addMethod({
|
|
||||||
isStatic: true,
|
|
||||||
name: `${prop}Validate`,
|
|
||||||
returnType: `boolean`,
|
|
||||||
parameters: [{
|
|
||||||
name: prop,
|
|
||||||
type: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`,
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
|
|
||||||
validateField.setBodyText((w) => {
|
|
||||||
w.write('return ');
|
|
||||||
const nonRequiredCall = isRequired ? prop : `!${prop} ? true : ${prop}`;
|
|
||||||
if (pType === 'string') {
|
|
||||||
if (isArray) {
|
|
||||||
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && (typeof p === 'string' && !!p.trim()), true)`);
|
|
||||||
} else {
|
|
||||||
if (typeof minLength === 'number' && maxLength) {
|
|
||||||
w.write(`(${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength})`);
|
|
||||||
}
|
|
||||||
if (typeof minLength !== 'number' || !maxLength) {
|
|
||||||
w.write(`${isRequired ? `typeof ${prop} === 'string'` : `!${prop} ? true : typeof ${prop} === 'string'`} && !!${nonRequiredCall}.trim()`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (pType === 'number') {
|
|
||||||
if (isArray) {
|
|
||||||
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'number', true)`);
|
|
||||||
} else {
|
|
||||||
if (typeof minimum === 'number' && maximum) {
|
|
||||||
w.write(`${isRequired ? `${prop} >= ${minimum} && ${prop} <= ${maximum}` : `!${prop} ? true : ((${prop} >= ${minimum}) && (${prop} <= ${maximum}))`}`);
|
|
||||||
}
|
|
||||||
if (typeof minimum !== 'number' || !maximum) {
|
|
||||||
w.write(`${isRequired ? `typeof ${prop} === 'number'` : `!${prop} ? true : typeof ${prop} === 'number'`}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (pType === 'boolean') {
|
|
||||||
w.write(`${isRequired ? `typeof ${prop} === 'boolean'` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
|
|
||||||
} else if (isEnum) {
|
|
||||||
if (isArray){
|
|
||||||
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && Object.keys(${pType}).includes(${prop}), true)`);
|
|
||||||
} else {
|
|
||||||
w.write(`${isRequired ? `Object.keys(${pType}).includes(${prop})` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.write(';');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (additionalProperties) {
|
|
||||||
const [
|
|
||||||
pType, isArray, isClass, isImport, isAdditional
|
|
||||||
] = schemaParamParser(additionalProperties, this.openapi);
|
|
||||||
const type = `Record<string, ${pType}${isArray ? '[]' : ''}>`;
|
|
||||||
|
|
||||||
entityClass.addProperty({
|
|
||||||
name: additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`,
|
|
||||||
isReadonly: true,
|
|
||||||
type: type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// add constructor;
|
|
||||||
const ctor = entityClass.addConstructor({
|
|
||||||
parameters: [{
|
|
||||||
name: 'props',
|
|
||||||
type: `I${sName}`,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
ctor.setBodyText((w) => {
|
|
||||||
if (additionalProperties) {
|
|
||||||
const [
|
|
||||||
pType, isArray, isClass, isImport, isAdditional
|
|
||||||
] = schemaParamParser(additionalProperties, this.openapi);
|
|
||||||
w.writeLine(`this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`} = Object.entries(props).reduce<Record<string, ${pType}>>((prev, [key, value]) => {`);
|
|
||||||
if (isClass) {
|
|
||||||
w.writeLine(` prev[key] = new ${pType}(value!);`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(' prev[key] = value!;')
|
|
||||||
}
|
|
||||||
w.writeLine(' return prev;');
|
|
||||||
w.writeLine('}, {})');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sortedSProps.forEach((sPropName) => {
|
|
||||||
const [
|
|
||||||
pType, isArray, isClass, , isAdditional
|
|
||||||
] = schemaParamParser(sProps[sPropName], this.openapi);
|
|
||||||
const req = (required && required.includes(sPropName))
|
|
||||||
|| sProps[sPropName].required;
|
|
||||||
if (!req) {
|
|
||||||
if ((pType === 'boolean' || pType === 'number' || pType ==='string') && !isClass && !isArray) {
|
|
||||||
w.writeLine(`if (typeof props.${sPropName} === '${pType}') {`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(`if (props.${sPropName}) {`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isAdditional) {
|
|
||||||
if (isArray && isClass) {
|
|
||||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => {
|
|
||||||
return { ...prev, [key]: new ${pType}(p[key])};
|
|
||||||
},{}))`);
|
|
||||||
} else if (isClass) {
|
|
||||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
|
|
||||||
return { ...prev, [key]: new ${pType}(props.${sPropName}[key])};
|
|
||||||
},{})`);
|
|
||||||
} else {
|
|
||||||
if (pType === 'string' && !isArray) {
|
|
||||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
|
|
||||||
return { ...prev, [key]: props.${sPropName}[key].trim()};
|
|
||||||
},{})`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
|
|
||||||
return { ...prev, [key]: props.${sPropName}[key]};
|
|
||||||
},{})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isArray && isClass) {
|
|
||||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => new ${pType}(p));`);
|
|
||||||
} else if (isClass) {
|
|
||||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = new ${pType}(props.${sPropName});`);
|
|
||||||
} else {
|
|
||||||
if (pType === 'string' && !isArray) {
|
|
||||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.trim();`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName};`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!req) {
|
|
||||||
w.writeLine('}');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// add serialize method;
|
|
||||||
const serialize = entityClass.addMethod({
|
|
||||||
isStatic: false,
|
|
||||||
name: 'serialize',
|
|
||||||
returnType: `I${sName}`,
|
|
||||||
});
|
|
||||||
serialize.setBodyText((w) => {
|
|
||||||
if (additionalProperties) {
|
|
||||||
const [
|
|
||||||
pType, isArray, isClass, isImport, isAdditional
|
|
||||||
] = schemaParamParser(additionalProperties, this.openapi);
|
|
||||||
w.writeLine(`return Object.entries(this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`}).reduce<Record<string, ${isClass ? 'I' : ''}${pType}>>((prev, [key, value]) => {`);
|
|
||||||
if (isClass) {
|
|
||||||
w.writeLine(` prev[key] = value.serialize();`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(' prev[key] = value;')
|
|
||||||
}
|
|
||||||
w.writeLine(' return prev;');
|
|
||||||
w.writeLine('}, {})');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
w.writeLine(`const data: I${sName} = {`);
|
|
||||||
const unReqFields: string[] = [];
|
|
||||||
sortedSProps.forEach((sPropName) => {
|
|
||||||
const req = (required && required.includes(sPropName))
|
|
||||||
|| sProps[sPropName].required;
|
|
||||||
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
|
||||||
if (!req) {
|
|
||||||
unReqFields.push(sPropName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isAdditional) {
|
|
||||||
if (isArray && isClass) {
|
|
||||||
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }))),`);
|
|
||||||
} else if (isClass) {
|
|
||||||
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce<Record<string, any>>((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {}),`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] })),`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isArray && isClass) {
|
|
||||||
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => p.serialize()),`);
|
|
||||||
} else if (isClass) {
|
|
||||||
w.writeLine(` ${sPropName}: this._${sPropName}.serialize(),`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(` ${sPropName}: this._${sPropName},`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
w.writeLine('};');
|
|
||||||
unReqFields.forEach((sPropName) => {
|
|
||||||
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
|
||||||
w.writeLine(`if (typeof this._${sPropName} !== 'undefined') {`);
|
|
||||||
if (isAdditional) {
|
|
||||||
if (isArray && isClass) {
|
|
||||||
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }), {}));`);
|
|
||||||
} else if (isClass) {
|
|
||||||
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {});`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] }), {});`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isArray && isClass) {
|
|
||||||
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => p.serialize());`);
|
|
||||||
} else if (isClass) {
|
|
||||||
w.writeLine(` data.${sPropName} = this._${sPropName}.serialize();`);
|
|
||||||
} else {
|
|
||||||
w.writeLine(` data.${sPropName} = this._${sPropName};`);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.writeLine(`}`);
|
|
||||||
});
|
|
||||||
w.writeLine('return data;');
|
|
||||||
});
|
|
||||||
|
|
||||||
// add validate method
|
|
||||||
const validate = entityClass.addMethod({
|
|
||||||
isStatic: false,
|
|
||||||
name: 'validate',
|
|
||||||
returnType: `string[]`,
|
|
||||||
})
|
|
||||||
validate.setBodyText((w) => {
|
|
||||||
if (additionalPropsOnly) {
|
|
||||||
w.writeLine('return []')
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
w.writeLine('const validate = {');
|
|
||||||
Object.keys(sProps || {}).forEach((sPropName) => {
|
|
||||||
const [pType, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
|
||||||
|
|
||||||
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
|
||||||
|
|
||||||
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
|
|
||||||
const nonRequiredCall = isRequired ? `this._${sPropName}` : `!this._${sPropName} ? true : this._${sPropName}`;
|
|
||||||
|
|
||||||
if (isArray && isClass) {
|
|
||||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && p.validate().length === 0, true),`);
|
|
||||||
} else if (isClass && !isAdditional) {
|
|
||||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.validate().length === 0,`);
|
|
||||||
} else {
|
|
||||||
if (pType === 'string') {
|
|
||||||
if (isArray) {
|
|
||||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'string', true),`);
|
|
||||||
} else {
|
|
||||||
if (typeof minLength === 'number' && maxLength) {
|
|
||||||
w.writeLine(` ${sPropName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`);
|
|
||||||
}
|
|
||||||
if (typeof minLength !== 'number' || !maxLength) {
|
|
||||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'string'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'string'`} && !this._${sPropName} ? true : this._${sPropName},`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (pType === 'number') {
|
|
||||||
if (isArray) {
|
|
||||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true),`);
|
|
||||||
} else {
|
|
||||||
if (typeof minimum === 'number' && maximum) {
|
|
||||||
w.writeLine(` ${sPropName}: ${isRequired ? `this._${sPropName} >= ${minimum} && this._${sPropName} <= ${maximum}` : `!this._${sPropName} ? true : ((this._${sPropName} >= ${minimum}) && (this._${sPropName} <= ${maximum}))`},`);
|
|
||||||
}
|
|
||||||
if (typeof minimum !== 'number' || !maximum) {
|
|
||||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'number'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'number'`},`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (pType === 'boolean') {
|
|
||||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'boolean'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'boolean'`},`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
w.writeLine('};');
|
|
||||||
w.writeLine('const isError: string[] = [];')
|
|
||||||
w.writeLine('Object.keys(validate).forEach((key) => {');
|
|
||||||
w.writeLine(' if (!(validate as any)[key]) {');
|
|
||||||
w.writeLine(' isError.push(key);');
|
|
||||||
w.writeLine(' }');
|
|
||||||
w.writeLine('});');
|
|
||||||
w.writeLine('return isError;');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// add update method;
|
|
||||||
const update = entityClass.addMethod({
|
|
||||||
isStatic: false,
|
|
||||||
name: 'update',
|
|
||||||
returnType: `${sName}`,
|
|
||||||
});
|
|
||||||
update.addParameter({
|
|
||||||
name: 'props',
|
|
||||||
type: additionalPropsOnly ? `I${sName}` : `Partial<I${sName}>`,
|
|
||||||
});
|
|
||||||
update.setBodyText((w) => { w.writeLine(`return new ${sName}({ ...this.serialize(), ...props });`); });
|
|
||||||
|
|
||||||
this.entities.push(entityFile);
|
|
||||||
};
|
|
||||||
|
|
||||||
save = () => {
|
|
||||||
this.entities.forEach(async (e) => {
|
|
||||||
await e.saveSync();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EntitiesGenerator;
|
|
|
@ -1,83 +0,0 @@
|
||||||
const toCamel = (s: string) => {
|
|
||||||
return s.replace(/([-_][a-z])/ig, ($1) => {
|
|
||||||
return $1.toUpperCase()
|
|
||||||
.replace('-', '')
|
|
||||||
.replace('_', '');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const capitalize = (s: string) => {
|
|
||||||
return s[0].toUpperCase() + s.slice(1);
|
|
||||||
};
|
|
||||||
const uncapitalize = (s: string) => {
|
|
||||||
return s[0].toLowerCase() + s.slice(1);
|
|
||||||
};
|
|
||||||
const TYPES = {
|
|
||||||
integer: 'number',
|
|
||||||
float: 'number',
|
|
||||||
number: 'number',
|
|
||||||
string: 'string',
|
|
||||||
boolean: 'boolean',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param schemaProp: valueof shema.properties[key]
|
|
||||||
* @param openApi: openapi object
|
|
||||||
* @returns [propType - basicType or import one, isArray, isClass, isImport]
|
|
||||||
*/
|
|
||||||
const schemaParamParser = (schemaProp: any, openApi: any): [string, boolean, boolean, boolean, boolean] => {
|
|
||||||
let type = '';
|
|
||||||
let isImport = false;
|
|
||||||
let isClass = false;
|
|
||||||
let isArray = false;
|
|
||||||
let isAdditional = false;
|
|
||||||
|
|
||||||
if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) {
|
|
||||||
const temp = (schemaProp.$ref || schemaProp.additionalProperties?.$ref).split('/');
|
|
||||||
|
|
||||||
if (schemaProp.additionalProperties) {
|
|
||||||
isAdditional = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
type = `${temp[temp.length - 1]}`;
|
|
||||||
|
|
||||||
const cl = openApi ? openApi.components.schemas[type] : {};
|
|
||||||
|
|
||||||
if (cl.$ref) {
|
|
||||||
const link = schemaParamParser(cl, openApi);
|
|
||||||
link.shift();
|
|
||||||
return [type, ...link] as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cl.type === 'string' && cl.enum) {
|
|
||||||
isImport = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cl.type === 'object' && !cl.oneOf) {
|
|
||||||
isClass = true;
|
|
||||||
isImport = true;
|
|
||||||
} else if (cl.type === 'array') {
|
|
||||||
const temp: any = schemaParamParser(cl.items, openApi);
|
|
||||||
type = `${temp[0]}`;
|
|
||||||
isArray = true;
|
|
||||||
isClass = isClass || temp[2];
|
|
||||||
isImport = isImport || temp[3];
|
|
||||||
}
|
|
||||||
} else if (schemaProp.type === 'array') {
|
|
||||||
const temp: any = schemaParamParser(schemaProp.items, openApi);
|
|
||||||
type = `${temp[0]}`;
|
|
||||||
isArray = true;
|
|
||||||
isClass = isClass || temp[2];
|
|
||||||
isImport = isImport || temp[3];
|
|
||||||
} else {
|
|
||||||
type = (TYPES as Record<any, string>)[schemaProp.type];
|
|
||||||
}
|
|
||||||
if (!type) {
|
|
||||||
// TODO: Fix bug with Error fields.
|
|
||||||
type = 'any';
|
|
||||||
// throw new Error('Failed to find entity type');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [type, isArray, isClass, isImport, isAdditional];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { TYPES, toCamel, capitalize, uncapitalize, schemaParamParser };
|
|
|
@ -1,226 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import {
|
|
||||||
Project,
|
|
||||||
VariableStatement,
|
|
||||||
SyntaxKind,
|
|
||||||
Node,
|
|
||||||
Statement,
|
|
||||||
ts,
|
|
||||||
Identifier,
|
|
||||||
SourceFile,
|
|
||||||
} from 'ts-morph';
|
|
||||||
import {
|
|
||||||
LOCALE_FOLDER_PATH,
|
|
||||||
TRANSLATOR_CLASS_NAME,
|
|
||||||
USE_INTL_NAME,
|
|
||||||
trimQuotes,
|
|
||||||
} from '../consts';
|
|
||||||
import { checkForms, AvailableLocales } from '../../src/localization/Translator';
|
|
||||||
|
|
||||||
const project = new Project({
|
|
||||||
tsConfigFilePath: './tsconfig.json',
|
|
||||||
});
|
|
||||||
|
|
||||||
let lang = 'ru';
|
|
||||||
let option = '';
|
|
||||||
|
|
||||||
if (process.argv.length > 2) {
|
|
||||||
lang = process.argv[2];
|
|
||||||
option = process.argv[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
const usedTranslations: string[] = [];
|
|
||||||
const usedPluralTranslations: string[] = [];
|
|
||||||
|
|
||||||
const problemFiles: string[] = [];
|
|
||||||
const sourceFiles = project.getSourceFiles();
|
|
||||||
const sourceFilesWithIntl = sourceFiles.filter((sf) => {
|
|
||||||
return !!sf.getImportDeclarations().find((id) => {
|
|
||||||
return !!id.getNamedImports().find((ni) => ni.getName() === USE_INTL_NAME)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const getFileUsedIntl = (statements: Statement<ts.Statement>[]) => {
|
|
||||||
statements.forEach((s) => {
|
|
||||||
if (s instanceof VariableStatement) {
|
|
||||||
s.forEachDescendant((node) => {
|
|
||||||
let intVariableDeclaration: Identifier = null;
|
|
||||||
switch (node.getKind()) {
|
|
||||||
case SyntaxKind.VariableDeclaration:
|
|
||||||
if (node.getSymbol()) {
|
|
||||||
const name = node.getSymbol().getName();
|
|
||||||
const callExp = node.getChildren().find((n) => n.getKind() === SyntaxKind.CallExpression);
|
|
||||||
if (callExp) {
|
|
||||||
const callExpIden = callExp.getChildren().find(n => n.getKind() === SyntaxKind.Identifier);
|
|
||||||
if (callExpIden && callExpIden.getSymbol().getName() === USE_INTL_NAME) {
|
|
||||||
intVariableDeclaration = node as Identifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (intVariableDeclaration) {
|
|
||||||
intVariableDeclaration.findReferencesAsNodes().forEach((fr) => {
|
|
||||||
if (fr instanceof Node) {
|
|
||||||
const parent = fr.getParentIfKind(SyntaxKind.PropertyAccessExpression);
|
|
||||||
if (parent && (parent.getName() === 'getMessage' || parent.getName() === 'getPlural')) {
|
|
||||||
const syntaxList = parent.getNextSiblings().find((n) => n.getKind() === SyntaxKind.SyntaxList);
|
|
||||||
if (syntaxList) {
|
|
||||||
const id = syntaxList.getChildren()[0];
|
|
||||||
if (id && id.getKind() !== SyntaxKind.StringLiteral) {
|
|
||||||
problemFiles.push(fr.getSourceFile().getFilePath());
|
|
||||||
}
|
|
||||||
if (id) {
|
|
||||||
usedTranslations.push(trimQuotes(id.getText()));
|
|
||||||
if (parent.getName() === 'getPlural') {
|
|
||||||
usedPluralTranslations.push(trimQuotes(id.getText()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFileUsedTranslations = (file: SourceFile) => {
|
|
||||||
const namedImport = file.getImportDeclarations().find((id) => !!id.getNamedImports().find((ni) => ni.getName() === TRANSLATOR_CLASS_NAME));
|
|
||||||
if (namedImport) {
|
|
||||||
const identifier = namedImport.getImportClause().getNamedImports().find((iden) => iden.getName() === TRANSLATOR_CLASS_NAME);
|
|
||||||
const translateReferences = identifier.getNodeProperty('name').findReferencesAsNodes();
|
|
||||||
if (translateReferences.length > 0) {
|
|
||||||
translateReferences.forEach((identifierNode) => {
|
|
||||||
if (identifierNode.getParentIfKind(SyntaxKind.TypeReference)) {
|
|
||||||
const translatorVariable = identifierNode.getParent().getPreviousSibling().getPreviousSiblingIfKind(SyntaxKind.Identifier);
|
|
||||||
if (translatorVariable) {
|
|
||||||
translatorVariable.findReferencesAsNodes().forEach((node) => {
|
|
||||||
const parent = node.getParentIfKind(SyntaxKind.PropertyAccessExpression);
|
|
||||||
if (parent && (parent.getName() === 'getMessage' || parent.getName() === 'getPlural')) {
|
|
||||||
|
|
||||||
const syntaxList = parent.getNextSiblings().find((n) => n.getKind() === SyntaxKind.SyntaxList);
|
|
||||||
if (syntaxList) {
|
|
||||||
const id = syntaxList.getChildren()[0];
|
|
||||||
if (id && id.getKind() !== SyntaxKind.StringLiteral) {
|
|
||||||
problemFiles.push(parent.getSourceFile().getFilePath());
|
|
||||||
}
|
|
||||||
if (id) {
|
|
||||||
usedTranslations.push(trimQuotes(id.getText()));
|
|
||||||
if (parent.getName() === 'getPlural') {
|
|
||||||
usedPluralTranslations.push(trimQuotes(id.getText()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceFilesWithIntl.forEach((file) => {
|
|
||||||
getFileUsedIntl(file.getStatements());
|
|
||||||
})
|
|
||||||
|
|
||||||
const sourceFilesWithTranslator = project.getSourceFiles().filter((sf) => {
|
|
||||||
return !!sf.getImportDeclarations().find((id) => {
|
|
||||||
return !!id.getNamedImports().find((ni) => ni.getName() === TRANSLATOR_CLASS_NAME)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
sourceFilesWithTranslator.forEach((file) => {
|
|
||||||
getFileUsedTranslations(file);
|
|
||||||
})
|
|
||||||
const filteredUsedTranslations = Array.from(new Set(usedTranslations));
|
|
||||||
const filteredUsedPluralTranslations = Array.from(new Set(usedPluralTranslations));
|
|
||||||
|
|
||||||
if (problemFiles.length) {
|
|
||||||
console.warn(`\n============== Files where translation id provided not as string ==============\n`);
|
|
||||||
console.log(problemFiles.join('\n'));
|
|
||||||
process.exit(255);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allFiles = fs.readdirSync(LOCALE_FOLDER_PATH);
|
|
||||||
// Use ru or needed language
|
|
||||||
const translationFile = allFiles.find((file) => file.includes(`${lang}.json`));
|
|
||||||
|
|
||||||
if (!translationFile) {
|
|
||||||
console.error('File not found');
|
|
||||||
process.exit(255);
|
|
||||||
}
|
|
||||||
|
|
||||||
const translationsObject = JSON.parse(fs.readFileSync(`./src/lib/intl/__locales/${translationFile}`, { flag: 'r+' }) as unknown as string);
|
|
||||||
const translations = {
|
|
||||||
locale: translationFile,
|
|
||||||
messages: Object.keys(translationsObject),
|
|
||||||
};
|
|
||||||
|
|
||||||
const someMessagesNotFound: string[] = [];
|
|
||||||
const notUsed: string[] = [];
|
|
||||||
const notFound: string[] = [];
|
|
||||||
const checkLocaleMessages = (locale: string, messages: string[]) => {
|
|
||||||
filteredUsedTranslations.forEach(f => {
|
|
||||||
if (!messages.includes(f)) {
|
|
||||||
notFound.push(f);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
messages.forEach(t => {
|
|
||||||
if (!filteredUsedTranslations.includes(t)) {
|
|
||||||
notUsed.push(t);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (notFound.length > 0) {
|
|
||||||
someMessagesNotFound.push(locale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const render = (data: string[], title: string) => {
|
|
||||||
console.log(`============ ${title} ============`);
|
|
||||||
console.table(data);
|
|
||||||
console.log(`============ ${title} ============`);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkLocaleMessages(translations.locale, translations.messages);
|
|
||||||
|
|
||||||
const checkPluralForm = () => {
|
|
||||||
const pluralFormWrong: string[] = [];
|
|
||||||
filteredUsedPluralTranslations.forEach((id) => {
|
|
||||||
const message = translationsObject[id];
|
|
||||||
if (!checkForms(message, lang as AvailableLocales, id)) {
|
|
||||||
pluralFormWrong.push(id)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return pluralFormWrong;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plural = checkPluralForm();
|
|
||||||
if (!option && (someMessagesNotFound.length || plural.length > 0 )) {
|
|
||||||
someMessagesNotFound.forEach(locale => console.error(`\nSome translatins for ${locale} was not found!\n`));
|
|
||||||
plural.forEach(id => console.error(`\nTranslation with id: "${id}" - have wrong number of plural forms!\n`));
|
|
||||||
process.exit(255);
|
|
||||||
}
|
|
||||||
if (option) {
|
|
||||||
switch (option) {
|
|
||||||
case '--show-missing': {
|
|
||||||
render(notFound, 'NotFound')
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '--show-unused': {
|
|
||||||
render(notUsed, 'notUsed')
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '--check-plurals': {
|
|
||||||
render(plural, 'Wrong Plural Form')
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
if (someMessagesNotFound.length) {
|
|
||||||
someMessagesNotFound.forEach(locale => console.error(`\nSome translatins for ${locale} was not found!\n\n`));
|
|
||||||
process.exit(255);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: './tsconfig.json',
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true
|
|
||||||
},
|
|
||||||
extraFileExtensions: ['mjs', 'tsx', 'ts'],
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: 'module'
|
|
||||||
},
|
|
||||||
plugins: ['react', '@typescript-eslint', 'import'],
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
commonjs: true,
|
|
||||||
es6: true,
|
|
||||||
es2020: true,
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
pragma: 'React',
|
|
||||||
version: 'detect',
|
|
||||||
},
|
|
||||||
'import/resolver': {
|
|
||||||
typescript: {
|
|
||||||
alwaysTryTypes: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'import/parsers': {
|
|
||||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
|
||||||
'@typescript-eslint/explicit-function-return-type': [0, { allowExpressions: true }],
|
|
||||||
'@typescript-eslint/indent': ['error', 4],
|
|
||||||
'@typescript-eslint/interface-name-prefix': [0, { prefixWithI: 'never' }],
|
|
||||||
'@typescript-eslint/no-explicit-any': [0],
|
|
||||||
'@typescript-eslint/naming-convention': [2, {
|
|
||||||
selector: 'enum', format: ['UPPER_CASE', 'PascalCase'],
|
|
||||||
}],
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 0,
|
|
||||||
'arrow-body-style': 'off',
|
|
||||||
'consistent-return': 0,
|
|
||||||
curly: [2, 'all'],
|
|
||||||
'default-case': 0,
|
|
||||||
'import/no-cycle': 0,
|
|
||||||
'import/prefer-default-export': 'off',
|
|
||||||
'import/no-named-as-default': 0,
|
|
||||||
indent: [0, 4],
|
|
||||||
'no-alert': 2,
|
|
||||||
'no-console': 2,
|
|
||||||
'no-debugger': 2,
|
|
||||||
'no-underscore-dangle': 'off',
|
|
||||||
'no-useless-escape': 'off',
|
|
||||||
'object-curly-newline': 'off',
|
|
||||||
'react-hooks/exhaustive-deps': 0,
|
|
||||||
'react/display-name': 0,
|
|
||||||
'react/jsx-indent-props': ['error', 4],
|
|
||||||
'react/jsx-indent': ['error', 4],
|
|
||||||
'react/jsx-one-expression-per-line': 'off',
|
|
||||||
'react/jsx-props-no-spreading': 0,
|
|
||||||
'react/prop-types': 'off',
|
|
||||||
'react/state-in-constructor': 'off',
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'airbnb-base',
|
|
||||||
'airbnb-typescript/base',
|
|
||||||
'airbnb/hooks',
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:import/errors',
|
|
||||||
'plugin:import/warnings',
|
|
||||||
'plugin:import/typescript',
|
|
||||||
],
|
|
||||||
globals: {},
|
|
||||||
};
|
|
|
@ -1,10 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
rules: {
|
|
||||||
'no-alert': 0,
|
|
||||||
'no-debugger': 0,
|
|
||||||
'no-console': 0,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'./common',
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: [
|
|
||||||
'./common.js',
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,40 +0,0 @@
|
||||||
const yaml = require('yaml');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const ZERO_HOST = '0.0.0.0';
|
|
||||||
const LOCALHOST = '127.0.0.1';
|
|
||||||
const DEFAULT_PORT = 80;
|
|
||||||
|
|
||||||
const importConfig = () => {
|
|
||||||
try {
|
|
||||||
const doc = yaml.parse(fs.readFileSync('../AdguardHome.yaml', 'utf8'));
|
|
||||||
const { bind_host, bind_port } = doc;
|
|
||||||
return {
|
|
||||||
bind_host,
|
|
||||||
bind_port,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
bind_host: ZERO_HOST,
|
|
||||||
bind_port: DEFAULT_PORT,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDevServerConfig = () => {
|
|
||||||
const { bind_host: host, bind_port: port } = importConfig();
|
|
||||||
const { DEV_SERVER_PORT } = process.env;
|
|
||||||
|
|
||||||
const devServerHost = host === ZERO_HOST ? LOCALHOST : host;
|
|
||||||
const devServerPort = 3000 || port + 8000;
|
|
||||||
|
|
||||||
return {
|
|
||||||
host: devServerHost,
|
|
||||||
port: devServerPort
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
importConfig,
|
|
||||||
getDevServerConfig
|
|
||||||
};
|
|
|
@ -1,74 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const tsconfig = require('../../tsconfig.json');
|
|
||||||
|
|
||||||
const RESOURCES_PATH = path.resolve(__dirname, '../../');
|
|
||||||
const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
|
|
||||||
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
install: './src/Install.tsx',
|
|
||||||
main: './src/App.tsx'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.tsx', '.ts', '.js', '.pcss'],
|
|
||||||
alias: Object.keys(tsconfig.compilerOptions.paths).reduce((aliases, key) => {
|
|
||||||
// Reduce to load aliases from ./tsconfig.json in appropriate for webpack form
|
|
||||||
const paths = tsconfig.compilerOptions.paths[key].map(p => p.replace('/*', ''));
|
|
||||||
aliases[key.replace('/*', '')] = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
'../../',
|
|
||||||
tsconfig.compilerOptions.baseUrl,
|
|
||||||
...paths,
|
|
||||||
);
|
|
||||||
return aliases;
|
|
||||||
}, {}),
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
use: 'ts-loader',
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(woff|woff2)$/,
|
|
||||||
use: [{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options:{
|
|
||||||
outputPath:'./',
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test:/\.(png|jpe?g|gif)$/,
|
|
||||||
exclude: /(node_modules)/,
|
|
||||||
use:[{
|
|
||||||
loader:'file-loader',
|
|
||||||
options:{
|
|
||||||
outputPath:'./images',
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
// new AntdDayjsWebpackPlugin()
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
inject: true,
|
|
||||||
cache: false,
|
|
||||||
chunks: ['main'],
|
|
||||||
template: HTML_PATH,
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
inject: true,
|
|
||||||
cache: false,
|
|
||||||
chunks: ['install'],
|
|
||||||
filename: 'install.html',
|
|
||||||
template: HTML_INSTALL_PATH,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,114 +0,0 @@
|
||||||
const history = require('connect-history-api-fallback');
|
|
||||||
const { merge } = require('webpack-merge');
|
|
||||||
const path = require('path');
|
|
||||||
const proxy = require('http-proxy-middleware');
|
|
||||||
const Webpack = require('webpack');
|
|
||||||
|
|
||||||
const { getDevServerConfig } = require('./helpers');
|
|
||||||
const baseConfig = require('./webpack.config.base');
|
|
||||||
|
|
||||||
const devHost = process.env.DEV_HOST
|
|
||||||
const target = getDevServerConfig();
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
target: devHost || `http://${target.host}:${target.port}`, // target host
|
|
||||||
changeOrigin: true, // needed for virtual hosted sites
|
|
||||||
};
|
|
||||||
const apiProxy = proxy.createProxyMiddleware(options);
|
|
||||||
|
|
||||||
module.exports = merge(baseConfig, {
|
|
||||||
mode: 'development',
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, '../../build2'),
|
|
||||||
filename: '[name].bundle.js',
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
noEmitOnErrors: true,
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
port: 4000,
|
|
||||||
historyApiFallback: true,
|
|
||||||
before: (app) => {
|
|
||||||
app.use('/control', apiProxy);
|
|
||||||
app.use(history({
|
|
||||||
rewrites: [
|
|
||||||
{
|
|
||||||
from: /\.(png|jpe?g|gif)$/,
|
|
||||||
to: (context) => {
|
|
||||||
const name = context.parsedUrl.pathname.split('/');
|
|
||||||
return `/images/${name[name.length - 1]}`
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
from: /\.(woff|woff2)$/,
|
|
||||||
to: (context) => {
|
|
||||||
const name = context.parsedUrl.pathname.split('/');
|
|
||||||
return `/${name[name.length - 1]}`
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
from: /\.(js|css)$/,
|
|
||||||
to: (context) => {
|
|
||||||
const name = context.parsedUrl.pathname.split('/');
|
|
||||||
return `/${name[name.length - 1]}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
devtool: 'eval-source-map',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
enforce: 'pre',
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
loader: 'eslint-loader',
|
|
||||||
options: {
|
|
||||||
configFile: path.resolve(__dirname, '../lint/dev.js'),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: (resource) => {
|
|
||||||
return (
|
|
||||||
resource.indexOf('.pcss')+1
|
|
||||||
|| resource.indexOf('.css')+1
|
|
||||||
|| resource.indexOf('.less')+1
|
|
||||||
) && !(resource.indexOf('.module.')+1);
|
|
||||||
},
|
|
||||||
use: ['style-loader', 'css-loader', 'postcss-loader', {
|
|
||||||
loader: 'less-loader',
|
|
||||||
options: {
|
|
||||||
javascriptEnabled: true,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.module\.p?css$/,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
{
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: {
|
|
||||||
modules: true,
|
|
||||||
sourceMap: true,
|
|
||||||
importLoaders: 1,
|
|
||||||
modules: {
|
|
||||||
localIdentName: "[name]__[local]___[hash:base64:5]",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'postcss-loader',
|
|
||||||
],
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new Webpack.DefinePlugin({
|
|
||||||
DEV: true,
|
|
||||||
'process.env.DEV_SERVER_PORT': JSON.stringify(3000),
|
|
||||||
}),
|
|
||||||
new Webpack.HotModuleReplacementPlugin(),
|
|
||||||
new Webpack.ProgressPlugin(),
|
|
||||||
],
|
|
||||||
});
|
|
|
@ -1,89 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
const { merge } = require('webpack-merge');
|
|
||||||
const baseConfig = require('./webpack.config.base');
|
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
|
||||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
|
||||||
const TerserJSPlugin = require('terser-webpack-plugin');
|
|
||||||
const Webpack = require('webpack');
|
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = merge(baseConfig, {
|
|
||||||
mode: 'production',
|
|
||||||
devtool: 'source-map',
|
|
||||||
stats: 'minimal',
|
|
||||||
performance : {
|
|
||||||
hints : false
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, '../../../build2/static'),
|
|
||||||
filename: '[name].bundle.[hash:5].js',
|
|
||||||
publicPath: '/'
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
minimizer: [new TerserJSPlugin({terserOptions: {
|
|
||||||
output: {
|
|
||||||
comments: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extractComments: false,
|
|
||||||
}), new OptimizeCSSAssetsPlugin({})],
|
|
||||||
splitChunks: {
|
|
||||||
cacheGroups: {
|
|
||||||
styles: {
|
|
||||||
name: 'styles',
|
|
||||||
test: /\.css$/,
|
|
||||||
chunks: 'all',
|
|
||||||
enforce: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: (resource) => {
|
|
||||||
return (
|
|
||||||
resource.indexOf('.pcss')+1
|
|
||||||
|| resource.indexOf('.css')+1
|
|
||||||
|| resource.indexOf('.less')+1
|
|
||||||
) && !(resource.indexOf('.module.')+1);
|
|
||||||
},
|
|
||||||
use: [{
|
|
||||||
loader: MiniCssExtractPlugin.loader,
|
|
||||||
}, 'css-loader', 'postcss-loader', {
|
|
||||||
loader: 'less-loader',
|
|
||||||
options: {
|
|
||||||
javascriptEnabled: true,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.module\.p?css$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: MiniCssExtractPlugin.loader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: {
|
|
||||||
modules: true,
|
|
||||||
sourceMap: true,
|
|
||||||
importLoaders: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'postcss-loader',
|
|
||||||
],
|
|
||||||
exclude: /node_modules/,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new Webpack.DefinePlugin({
|
|
||||||
DEV: false,
|
|
||||||
}),
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: '[name].[hash:5].css',
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
});
|
|
|
@ -1,18 +0,0 @@
|
||||||
import './main.pcss';
|
|
||||||
import './lib/ant/ant.less';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import Store, { storeValue } from 'Store';
|
|
||||||
import './lib/ant';
|
|
||||||
|
|
||||||
import App from './components/App';
|
|
||||||
|
|
||||||
const Container = () => {
|
|
||||||
return (
|
|
||||||
<Store.Provider value={storeValue}>
|
|
||||||
<App/>
|
|
||||||
</Store.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactDOM.render(<Container />, document.getElementById('app'));
|
|
|
@ -1,18 +0,0 @@
|
||||||
import './main.pcss';
|
|
||||||
import './lib/ant/ant.less';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import Store, { storeValue } from 'Store/installStore';
|
|
||||||
import './lib/ant';
|
|
||||||
|
|
||||||
import Install from './components/Install';
|
|
||||||
|
|
||||||
const Container = () => {
|
|
||||||
return (
|
|
||||||
<Store.Provider value={storeValue}>
|
|
||||||
<Install/>
|
|
||||||
</Store.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactDOM.render(<Container />, document.getElementById('app'));
|
|
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
|
@ -1,20 +0,0 @@
|
||||||
import React, { FC } from 'react';
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Icons from 'Common/ui/Icons';
|
|
||||||
import Routes from './Routes';
|
|
||||||
|
|
||||||
import { ErrorBoundary } from './Errors';
|
|
||||||
|
|
||||||
const App: FC = () => {
|
|
||||||
return (
|
|
||||||
<ErrorBoundary>
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes />
|
|
||||||
<Icons />
|
|
||||||
</BrowserRouter>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,136 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Row, Col } from 'antd';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
|
|
||||||
import Store from 'Store';
|
|
||||||
import { InnerLayout } from 'Common/ui/layouts';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
import { BlockCard, TopDomains, BlockedQueries, TopClients, ServerStatistics } from './components';
|
|
||||||
|
|
||||||
const Dashboard:FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const {
|
|
||||||
dashboard: { stats, filteringConfig },
|
|
||||||
system: { status },
|
|
||||||
ui: { intl },
|
|
||||||
} = store;
|
|
||||||
|
|
||||||
if (!stats || !filteringConfig) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
numBlockedFiltering,
|
|
||||||
numReplacedParental,
|
|
||||||
numReplacedSafebrowsing,
|
|
||||||
replacedParental,
|
|
||||||
replacedSafebrowsing,
|
|
||||||
avgProcessingTime,
|
|
||||||
blockedFiltering,
|
|
||||||
|
|
||||||
topBlockedDomains,
|
|
||||||
topQueriedDomains,
|
|
||||||
dnsQueries,
|
|
||||||
numDnsQueries,
|
|
||||||
|
|
||||||
} = stats;
|
|
||||||
|
|
||||||
const { filters } = filteringConfig!;
|
|
||||||
const allFilters = filters?.length;
|
|
||||||
const allRules = filters?.reduce((prev, e) => prev + (e.rulesCount || 0), 0);
|
|
||||||
const enabled = filters?.filter((e) => e.enabled).length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InnerLayout title={`AdGuard Home ${status?.version}`}>
|
|
||||||
<div className={theme.content.container}>
|
|
||||||
<Row gutter={[24, 24]}>
|
|
||||||
<Col span={24} md={12}>
|
|
||||||
<TopDomains
|
|
||||||
title={intl.getMessage('stats_query_domain')}
|
|
||||||
overal={numDnsQueries!}
|
|
||||||
chartData={dnsQueries!}
|
|
||||||
tableData={topQueriedDomains!}
|
|
||||||
color={theme.chartColors.green}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={12}>
|
|
||||||
<TopDomains
|
|
||||||
useValueColor
|
|
||||||
title={intl.getMessage('top_blocked_domains')}
|
|
||||||
overal={numBlockedFiltering!}
|
|
||||||
chartData={blockedFiltering!}
|
|
||||||
tableData={topBlockedDomains!}
|
|
||||||
color={theme.chartColors.red}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row gutter={[24, 24]}>
|
|
||||||
<Col span={24} md={18}>
|
|
||||||
<Row gutter={[24, 24]}>
|
|
||||||
<Col span={24} md={8}>
|
|
||||||
<BlockCard
|
|
||||||
title={intl.getMessage('dashboard_blocked_ads')}
|
|
||||||
overal={numBlockedFiltering!}
|
|
||||||
data={blockedFiltering!}
|
|
||||||
color={theme.chartColors.red}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={8}>
|
|
||||||
<BlockCard
|
|
||||||
title={intl.getMessage('dashboard_blocked_trackers')}
|
|
||||||
overal={numBlockedFiltering!}
|
|
||||||
data={blockedFiltering!}
|
|
||||||
color={theme.chartColors.orange}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={8}>
|
|
||||||
<BlockCard
|
|
||||||
title={intl.getMessage('stats_adult')}
|
|
||||||
overal={numReplacedParental!}
|
|
||||||
data={replacedParental!}
|
|
||||||
color={theme.chartColors.purple}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={8}>
|
|
||||||
<BlockCard
|
|
||||||
title={intl.getMessage('stats_malware_phishing')}
|
|
||||||
overal={numReplacedSafebrowsing!}
|
|
||||||
data={replacedSafebrowsing!}
|
|
||||||
color={theme.chartColors.red}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={8}>
|
|
||||||
<BlockCard
|
|
||||||
title={intl.getMessage('average_processing_time')}
|
|
||||||
overal={`${Math.round(avgProcessingTime! * 100)} ${intl.getMessage('milliseconds_abbreviation')}`}
|
|
||||||
data={blockedFiltering!}
|
|
||||||
color={theme.chartColors.green}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={8}>
|
|
||||||
<BlockCard
|
|
||||||
title={intl.getMessage('dashboard_filter_rules')}
|
|
||||||
overal={allRules!}
|
|
||||||
text={intl.getMessage('dashboard_filter_rules_count', { enabled, all: allFilters })}
|
|
||||||
color={theme.chartColors.green}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={6}>
|
|
||||||
{/* TODO: fix chart */}
|
|
||||||
<BlockedQueries
|
|
||||||
other={numBlockedFiltering! / 3}
|
|
||||||
ads={numBlockedFiltering!}
|
|
||||||
trackers={numBlockedFiltering!}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<TopClients />
|
|
||||||
<ServerStatistics />
|
|
||||||
</div>
|
|
||||||
</InnerLayout>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Dashboard;
|
|
|
@ -1,20 +0,0 @@
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
padding: 24px;
|
|
||||||
background-color: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: var(--gray700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overal {
|
|
||||||
font-size: 30px;
|
|
||||||
line-height: 38px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
color: var(--gray900);
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import React, { FC } from 'react';
|
|
||||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
|
||||||
|
|
||||||
import s from './BlockCard.module.pcss';
|
|
||||||
|
|
||||||
interface BlockCardProps {
|
|
||||||
overal: number | string;
|
|
||||||
data?: number[];
|
|
||||||
text?: string;
|
|
||||||
color?: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BlockCard: FC<BlockCardProps> = ({ overal, data, color, title, text }) => {
|
|
||||||
return (
|
|
||||||
<div className={s.container}>
|
|
||||||
<div className={s.title}>{title}</div>
|
|
||||||
<div className={s.overal}>{overal}</div>
|
|
||||||
{data && (
|
|
||||||
<ResponsiveContainer width="100%" height={25}>
|
|
||||||
<AreaChart data={data.map((n) => ({ name: 'data', value: n }))}>
|
|
||||||
<Area dataKey="value" stroke={color} fill={color} dot={false} />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
)}
|
|
||||||
{text && (
|
|
||||||
<div>
|
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlockCard;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as BlockCard } from './BlockCard';
|
|
|
@ -1,16 +0,0 @@
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
padding: 24px;
|
|
||||||
background-color: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: var(--gray700);
|
|
||||||
}
|
|
||||||
.pie {
|
|
||||||
padding: 34px 0px;
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import theme from 'Lib/theme';
|
|
||||||
import React, { FC, useContext, useState } from 'react';
|
|
||||||
import { PieChart, Pie, ResponsiveContainer, Sector, Cell } from 'recharts';
|
|
||||||
|
|
||||||
import Store from 'Store';
|
|
||||||
|
|
||||||
import s from './BlockedQueries.module.pcss';
|
|
||||||
|
|
||||||
interface BlockCardProps {
|
|
||||||
ads: number;
|
|
||||||
trackers: number;
|
|
||||||
other: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderActiveShape = (props: any): any => {
|
|
||||||
const {
|
|
||||||
cx, cy, innerRadius, outerRadius, startAngle, endAngle,
|
|
||||||
fill, payload, percent,
|
|
||||||
} = props;
|
|
||||||
return (
|
|
||||||
<g>
|
|
||||||
<text x={cx} y={cy - 11} dy={8} textAnchor="middle" fill={fill}>{payload.name}</text>
|
|
||||||
<text x={cx} y={cy + 18} dy={8} fontSize={24} textAnchor="middle" >{Math.round(percent * 100)}%</text>
|
|
||||||
<Sector
|
|
||||||
cx={cx}
|
|
||||||
cy={cy}
|
|
||||||
innerRadius={innerRadius + 5}
|
|
||||||
outerRadius={outerRadius + 5}
|
|
||||||
startAngle={startAngle + 1}
|
|
||||||
endAngle={endAngle - 1}
|
|
||||||
fill={fill}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const BlockedQueries: FC<BlockCardProps> = ({ ads, trackers, other }) => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
|
||||||
const { ui: { intl } } = store;
|
|
||||||
const data = [
|
|
||||||
{ name: intl.getMessage('other'), value: other, color: theme.chartColors.gray700 },
|
|
||||||
{ name: intl.getMessage('ads'), value: ads, color: theme.chartColors.red },
|
|
||||||
{ name: intl.getMessage('trackers'), value: trackers, color: theme.chartColors.orange },
|
|
||||||
];
|
|
||||||
const onChart: any = (_: any, index: number) => {
|
|
||||||
setActiveIndex(index);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className={s.container}>
|
|
||||||
<div className={s.title}>{intl.getMessage('dashboard_blocked_queries')}</div>
|
|
||||||
<div className={s.pie}>
|
|
||||||
<ResponsiveContainer width="100%" height={190}>
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
activeIndex={activeIndex}
|
|
||||||
data={data}
|
|
||||||
dataKey="value"
|
|
||||||
nameKey="name"
|
|
||||||
innerRadius={60}
|
|
||||||
outerRadius={80}
|
|
||||||
activeShape={renderActiveShape}
|
|
||||||
onClick={onChart}
|
|
||||||
>
|
|
||||||
{data.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlockedQueries;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as BlockedQueries } from './BlockedQueries';
|
|
|
@ -1,46 +0,0 @@
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
background-color: var(--white);
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
padding: 24px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 22px;
|
|
||||||
border-bottom: 1px solid var(--gray300);
|
|
||||||
color: var(--gray900);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 24px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardBorder {
|
|
||||||
border-right: 1px solid var(--gray300);
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardTitle {
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardDesc {
|
|
||||||
color: var(--gray700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardValue {
|
|
||||||
color: var(--gray900);
|
|
||||||
font-size: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Row, Col } from 'antd';
|
|
||||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
|
||||||
|
|
||||||
import Store from 'Store';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
|
|
||||||
import s from './ServerStatistics.module.pcss';
|
|
||||||
|
|
||||||
const ServerStatistics: FC = () => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { ui: { intl } } = store;
|
|
||||||
|
|
||||||
const data = [0, 10, 2, 14, 12, 24, 5, 8, 10, 0, 3, 5, 7, 8, 3];
|
|
||||||
return (
|
|
||||||
<div className={s.container}>
|
|
||||||
<div className={s.title}>{intl.getMessage('dashboard_server_statistics')}</div>
|
|
||||||
<Row>
|
|
||||||
<Col span={24} md={6} className={s.cardBorder}>
|
|
||||||
<div className={s.card}>
|
|
||||||
<div className={s.cardTitle}>
|
|
||||||
Average server load
|
|
||||||
</div>
|
|
||||||
<div className={s.cardDesc}>
|
|
||||||
<div>
|
|
||||||
Processes: 213
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Cores: 2
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ResponsiveContainer width="100%" height={25} className={s.chart}>
|
|
||||||
<AreaChart data={data.map((n) => ({ name: 'data', value: n }))}>
|
|
||||||
<Area dataKey="value" stroke={theme.chartColors.green} fill={theme.chartColors.green} dot={false} />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={6} className={s.cardBorder}>
|
|
||||||
<div className={s.card}>
|
|
||||||
<div className={s.cardTitle}>
|
|
||||||
Memory usage
|
|
||||||
</div>
|
|
||||||
<div className={s.cardValue}>
|
|
||||||
236 Mb
|
|
||||||
</div>
|
|
||||||
<ResponsiveContainer width="100%" height={25} className={s.chart}>
|
|
||||||
<AreaChart data={data.map((n) => ({ name: 'data', value: n }))}>
|
|
||||||
<Area dataKey="value" stroke={theme.chartColors.orange} fill={theme.chartColors.orange} dot={false} />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={6} className={s.cardBorder}>
|
|
||||||
<div className={s.card}>
|
|
||||||
<div className={s.cardTitle}>
|
|
||||||
DNS cashe size
|
|
||||||
</div>
|
|
||||||
<div className={s.cardValue}>
|
|
||||||
2 363 records
|
|
||||||
</div>
|
|
||||||
<div className={s.cardDesc}>
|
|
||||||
<div>
|
|
||||||
32 Mb
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col span={24} md={6} className={s.cardBorder}>
|
|
||||||
<div className={s.card}>
|
|
||||||
<div className={s.cardTitle}>
|
|
||||||
Upstream servers data
|
|
||||||
</div>
|
|
||||||
<div className={s.cardDesc}>
|
|
||||||
<div>
|
|
||||||
Processes: 213
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Cores: 2
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServerStatistics;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as ServerStatistics } from './ServerStatistics';
|
|
|
@ -1,43 +0,0 @@
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
background-color: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 22px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
padding: 24px;
|
|
||||||
color: var(--gray900);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableTitle {
|
|
||||||
color: var(--gray700);
|
|
||||||
background-color: #fafafa;
|
|
||||||
padding: 24px;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableGrid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 4fr 1fr 1fr 1.5fr 1fr .5fr;
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-bottom: 1px solid var(--gray300);
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ids {
|
|
||||||
color: var(--gray700)
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
|
|
||||||
import Store from 'Store';
|
|
||||||
|
|
||||||
import s from './TopClients.module.pcss';
|
|
||||||
|
|
||||||
const TopClients: FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { ui: { intl }, dashboard } = store;
|
|
||||||
const { clientsInfo, stats } = dashboard;
|
|
||||||
const topClients = new Map();
|
|
||||||
stats?.topClients?.forEach((client) => {
|
|
||||||
const [id, requests] = Object.entries(client.numberData);
|
|
||||||
topClients.set(id, requests);
|
|
||||||
});
|
|
||||||
const clients = Array.from(clientsInfo.entries());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={s.container}>
|
|
||||||
<div className={s.title}>{intl.getMessage('Top Clients')}</div>
|
|
||||||
<div className={s.table}>
|
|
||||||
<div className={cn(s.tableTitle, s.tableGrid)}>
|
|
||||||
<div>{intl.getMessage('client_table_header')}</div>
|
|
||||||
<div>{intl.getMessage('requests')}</div>
|
|
||||||
<div>{intl.getMessage('show_blocked_responses')}</div>
|
|
||||||
<div>%</div>
|
|
||||||
<div/>
|
|
||||||
<div/>
|
|
||||||
</div>
|
|
||||||
{clients.map(([id, c]) => {
|
|
||||||
const request = topClients.get(id);
|
|
||||||
return (
|
|
||||||
<div className={s.tableGrid} key={id}>
|
|
||||||
<div>
|
|
||||||
{c.name}
|
|
||||||
<div className={s.ids}>
|
|
||||||
{c.ids?.map((cid) => (
|
|
||||||
<div key={cid}>{cid}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{request}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
API
|
|
||||||
{/* TODO: api */}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
API / {request}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button>
|
|
||||||
{intl.getMessage('Block')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default TopClients;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as TopClients } from './TopClients';
|
|
|
@ -1,62 +0,0 @@
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
background-color: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
padding: 24px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 22px;
|
|
||||||
border-bottom: 1px solid var(--gray300);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: var(--gray900);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 24px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.overal {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 32px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
color: var(--gray900);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
position: relative;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 280px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableHeader {
|
|
||||||
/* TODO: color */
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
width: inherit;
|
|
||||||
background-color: #fafafa;
|
|
||||||
font-weight: 500;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableRow {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 3fr 1fr 1.5fr;
|
|
||||||
grid-column-gap: 10px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-bottom: 1px solid var(--gray300);
|
|
||||||
}
|
|
||||||
|
|
||||||
.domain {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Progress } from 'antd';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
|
||||||
|
|
||||||
import TopArrayEntry from 'Entities/TopArrayEntry';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
import Store from 'Store';
|
|
||||||
|
|
||||||
import s from './TopDomains.module.pcss';
|
|
||||||
|
|
||||||
interface TopDomainsProps {
|
|
||||||
title: string;
|
|
||||||
overal: number;
|
|
||||||
chartData: number[];
|
|
||||||
tableData: TopArrayEntry[];
|
|
||||||
color: string;
|
|
||||||
useValueColor?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TopDomains: FC<TopDomainsProps> = (
|
|
||||||
{ title, overal, chartData, tableData, color, useValueColor },
|
|
||||||
) => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { ui: { intl } } = store;
|
|
||||||
const data = tableData.map((e) => {
|
|
||||||
const [domain, value] = Object.entries(e.numberData)[0];
|
|
||||||
return { domain, value };
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={s.container}>
|
|
||||||
<div className={s.title}>{title}</div>
|
|
||||||
<div className={s.content}>
|
|
||||||
<div className={s.overal}>
|
|
||||||
{overal.toLocaleString('en')}
|
|
||||||
<ResponsiveContainer width="100%" height={45}>
|
|
||||||
<AreaChart data={chartData.map((n) => ({ name: 'data', value: n }))}>
|
|
||||||
<Area dataKey="value" stroke={color} fill={color} dot={false} strokeWidth={2} />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
<div className={s.table}>
|
|
||||||
<div className={cn(s.tableHeader, s.tableRow)}>
|
|
||||||
<div>
|
|
||||||
{intl.getMessage('domain')}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{intl.getMessage('all_queries')}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{data.map(({ domain, value }) => (
|
|
||||||
<div className={s.tableRow} key={domain}>
|
|
||||||
<div className={s.domain}>{domain}</div>
|
|
||||||
<div style={{ color: useValueColor ? color : 'initial' }}>{value}</div>
|
|
||||||
<Progress
|
|
||||||
percent={Math.round((value / overal) * 100)}
|
|
||||||
strokeLinecap="square"
|
|
||||||
strokeColor={theme.chartColors.gray700}
|
|
||||||
trailColor={theme.chartColors.gray300}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TopDomains;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as TopDomains } from './TopDomains';
|
|
|
@ -1,5 +0,0 @@
|
||||||
export { BlockCard } from './BlockCard';
|
|
||||||
export { TopClients } from './TopClients';
|
|
||||||
export { TopDomains } from './TopDomains';
|
|
||||||
export { BlockedQueries } from './BlockedQueries';
|
|
||||||
export { ServerStatistics } from './ServerStatistics';
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Dashboard';
|
|
|
@ -1,31 +0,0 @@
|
||||||
import React, { Component, ReactNode } from 'react';
|
|
||||||
import cn from 'classnames';
|
|
||||||
|
|
||||||
import s from './Errors.module.pcss';
|
|
||||||
|
|
||||||
export default class ErrorBoundary extends Component {
|
|
||||||
state = {
|
|
||||||
isError: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromError(): { isError: boolean } {
|
|
||||||
return { isError: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): ReactNode {
|
|
||||||
const { isError } = this.state;
|
|
||||||
const { children } = this.props;
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
return (
|
|
||||||
<div className={cn(s.content, s.content_boundary)}>
|
|
||||||
<div className={s.title}>
|
|
||||||
Something went wrong
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 455px;
|
|
||||||
min-height: calc(100vh - var(--header-height) - 64px);
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&_boundary {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
@media (--s-viewport) {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.code {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
font-size: 120px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 108px;
|
|
||||||
color: var(--morning);
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
@media (--s-viewport) {
|
|
||||||
margin-bottom: 54px;
|
|
||||||
font-size: 180px;
|
|
||||||
line-height: 162px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
width: 160px;
|
|
||||||
height: 173px;
|
|
||||||
|
|
||||||
@media (--s-viewport) {
|
|
||||||
width: 243px;
|
|
||||||
height: 262px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&_code {
|
|
||||||
position: absolute;
|
|
||||||
top: -20px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
|
|
||||||
@media (--s-viewport) {
|
|
||||||
top: -34px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
max-width: 384px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--gray);
|
|
||||||
|
|
||||||
@media (--s-viewport) {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as ErrorBoundary } from './ErrorBoundary';
|
|
|
@ -1,81 +0,0 @@
|
||||||
.header {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
color: var(--gray900);
|
|
||||||
background-color: var(--white);
|
|
||||||
box-shadow: 0 1px 4px 0 rgba(0, 21, 41, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top,
|
|
||||||
.bottom {
|
|
||||||
padding: 12px 16px;
|
|
||||||
|
|
||||||
@media (--l-viewport) {
|
|
||||||
padding: 12px 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.top {
|
|
||||||
background-color: var(--black);
|
|
||||||
|
|
||||||
@media (--l-viewport) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
@media (--l-viewport) {
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
height: var(--header-height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
@media (--l-viewport) {
|
|
||||||
margin: 0 16px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action {
|
|
||||||
min-width: 80px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.languages,
|
|
||||||
.user {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
@media (--l-viewport) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user {
|
|
||||||
margin-right: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
color: var(--white);
|
|
||||||
background-color: transparent;
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
color: var(--gray400);
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import { MenuOutlined } from '@ant-design/icons';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
|
|
||||||
import { Icon, LangSelect } from 'Common/ui';
|
|
||||||
import Store from 'Store';
|
|
||||||
|
|
||||||
import s from './Header.module.pcss';
|
|
||||||
|
|
||||||
const Header: FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { ui: { intl }, system, ui } = store;
|
|
||||||
const { status, profile } = system;
|
|
||||||
|
|
||||||
const updateServerStatus = () => {
|
|
||||||
system.switchServerStatus(!status?.protectionEnabled);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={s.header}>
|
|
||||||
<div className={s.top}>
|
|
||||||
<Button
|
|
||||||
icon={<MenuOutlined />}
|
|
||||||
className={s.menu}
|
|
||||||
onClick={() => ui.toggleSidebar()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={s.bottom}>
|
|
||||||
<div className={s.status}>
|
|
||||||
<Icon icon="logo_shield" className={s.icon} />
|
|
||||||
{status?.protectionEnabled
|
|
||||||
? intl.getMessage('header_adguard_status_enabled')
|
|
||||||
: intl.getMessage('header_adguard_status_disabled')}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="ghost"
|
|
||||||
size="small"
|
|
||||||
className={s.action}
|
|
||||||
onClick={updateServerStatus}
|
|
||||||
>
|
|
||||||
{status?.protectionEnabled
|
|
||||||
? intl.getMessage('disable')
|
|
||||||
: intl.getMessage('enable')}
|
|
||||||
</Button>
|
|
||||||
{profile?.name && (
|
|
||||||
<div className={s.user}>
|
|
||||||
<Icon icon="user" className={s.icon} />
|
|
||||||
{profile?.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={s.languages}>
|
|
||||||
<LangSelect />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Header;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Header';
|
|
|
@ -1,65 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import cn from 'classnames';
|
|
||||||
|
|
||||||
import { CommonLayout } from 'Common/ui/layouts';
|
|
||||||
import { code } from 'Common/formating';
|
|
||||||
import { Link } from 'Common/ui';
|
|
||||||
import Store from 'Store';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
|
|
||||||
import s from './Login.module.pcss';
|
|
||||||
import { RoutePath } from '../Routes/Paths';
|
|
||||||
|
|
||||||
const ForgotPassword: FC = () => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { ui: { intl } } = store;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommonLayout className={cn(theme.content.content, theme.content.content_auth)}>
|
|
||||||
<div className={cn(theme.content.container, theme.content.container_auth)}>
|
|
||||||
<div className={s.title}>
|
|
||||||
{intl.getMessage('login_password_title')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className={s.paragraph}>
|
|
||||||
{intl.getMessage('login_password_hash')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className={s.list}>
|
|
||||||
<div className={s.step}>
|
|
||||||
{intl.getMessage('login_password_step_1')}
|
|
||||||
</div>
|
|
||||||
<div className={s.step}>
|
|
||||||
{intl.getMessage('login_password_step_2', { code })}
|
|
||||||
</div>
|
|
||||||
<div className={s.step}>
|
|
||||||
{intl.getMessage('login_password_step_3', { code })}
|
|
||||||
</div>
|
|
||||||
<div className={s.step}>
|
|
||||||
{intl.getMessage('login_password_step_4')}
|
|
||||||
</div>
|
|
||||||
<div className={s.step}>
|
|
||||||
{intl.getMessage('login_password_step_5')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className={s.paragraph}>
|
|
||||||
{intl.getMessage('login_password_result')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Link to={RoutePath.Login}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
block
|
|
||||||
>
|
|
||||||
{intl.getMessage('back')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</CommonLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ForgotPassword;
|
|
|
@ -1,34 +0,0 @@
|
||||||
.title {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 28px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&_form {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-top: 32px;
|
|
||||||
font-size: 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph {
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 0 0 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding-left: 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
display: list-item;
|
|
||||||
list-style: decimal;
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
|
||||||
import cn from 'classnames';
|
|
||||||
|
|
||||||
import { Input } from 'Common/controls';
|
|
||||||
import { CommonLayout } from 'Common/ui/layouts';
|
|
||||||
import { Link } from 'Common/ui';
|
|
||||||
import { RoutePath } from 'Components/App/Routes/Paths';
|
|
||||||
import Store from 'Store';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
|
|
||||||
import s from './Login.module.pcss';
|
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
name: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Login: FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { ui: { intl }, login } = store;
|
|
||||||
|
|
||||||
const onSubmit = async (values: FormValues, { setSubmitting }: FormikHelpers<FormValues>) => {
|
|
||||||
const { name, password } = values;
|
|
||||||
|
|
||||||
const error = await login.login({
|
|
||||||
name,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialValues: FormValues = {
|
|
||||||
name: '',
|
|
||||||
password: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommonLayout className={cn(theme.content.content, theme.content.content_auth)}>
|
|
||||||
<div className={cn(theme.content.container, theme.content.container_auth)}>
|
|
||||||
<div className={cn(s.title, s.title_form)}>
|
|
||||||
{intl.getMessage('login')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Formik
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({
|
|
||||||
values,
|
|
||||||
handleSubmit,
|
|
||||||
setFieldValue,
|
|
||||||
isSubmitting,
|
|
||||||
}) => (
|
|
||||||
<form noValidate onSubmit={handleSubmit}>
|
|
||||||
<Input
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
placeholder={intl.getMessage('username')}
|
|
||||||
value={values.name}
|
|
||||||
onChange={(v) => setFieldValue('name', v)}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
placeholder={intl.getMessage('password')}
|
|
||||||
value={values.password}
|
|
||||||
onChange={(v) => setFieldValue('password', v)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
htmlType="submit"
|
|
||||||
disabled={!values.name || !values.password || isSubmitting}
|
|
||||||
block
|
|
||||||
>
|
|
||||||
{intl.getMessage('sign_in')}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
|
|
||||||
<div className={theme.text.center}>
|
|
||||||
<Link
|
|
||||||
to={RoutePath.ForgotPassword}
|
|
||||||
className={cn(theme.link.link, theme.link.gray, s.link)}
|
|
||||||
>
|
|
||||||
{intl.getMessage('login_password_link')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CommonLayout>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Login;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default as Login } from './Login';
|
|
||||||
export { default as ForgotPassword } from './ForgotPassword';
|
|
|
@ -1,63 +0,0 @@
|
||||||
import qs from 'qs';
|
|
||||||
import { Locale } from 'Localization';
|
|
||||||
|
|
||||||
const BasicPath = '/';
|
|
||||||
const pathBuilder = (path: string) => (`${BasicPath}${path}`);
|
|
||||||
|
|
||||||
export enum RoutePath {
|
|
||||||
Dashboard = 'Dashboard',
|
|
||||||
FiltersBlocklist = 'FiltersBlocklist',
|
|
||||||
FiltersAllowlist = 'FiltersAllowlist',
|
|
||||||
FiltersRewrites = 'FiltersRewrites',
|
|
||||||
FiltersServices = 'FiltersServices',
|
|
||||||
FiltersCustom = 'FiltersCustom',
|
|
||||||
QueryLog = 'QueryLog',
|
|
||||||
SetupGuide = 'SetupGuide',
|
|
||||||
SettingsGeneral = 'SettingsGeneral',
|
|
||||||
SettingsDns = 'SettingsDns',
|
|
||||||
SettingsEncryption = 'SettingsEncryption',
|
|
||||||
SettingsClients = 'SettingsClients',
|
|
||||||
SettingsDhcp = 'SettingsDhcp',
|
|
||||||
Login = 'Login',
|
|
||||||
ForgotPassword = 'ForgotPassword',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Paths: Record<RoutePath, string> = {
|
|
||||||
Dashboard: pathBuilder('dashboard'),
|
|
||||||
FiltersBlocklist: pathBuilder('filters/blocklists'),
|
|
||||||
FiltersAllowlist: pathBuilder('filters/allowlists'),
|
|
||||||
FiltersRewrites: pathBuilder('filters/rewrites'),
|
|
||||||
FiltersServices: pathBuilder('filters/services'),
|
|
||||||
FiltersCustom: pathBuilder('filters/custom'),
|
|
||||||
QueryLog: pathBuilder('logs'),
|
|
||||||
SetupGuide: pathBuilder('guide'),
|
|
||||||
SettingsGeneral: pathBuilder('settings/general'),
|
|
||||||
SettingsDns: pathBuilder('settings/dns'),
|
|
||||||
SettingsEncryption: pathBuilder('settings/encryption'),
|
|
||||||
SettingsClients: pathBuilder('settings/clients'),
|
|
||||||
SettingsDhcp: pathBuilder('settings/dhcp'),
|
|
||||||
Login: pathBuilder(''),
|
|
||||||
ForgotPassword: pathBuilder('forgot_password'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum LinkParamsKeys {}
|
|
||||||
export enum QueryParams {}
|
|
||||||
export type LinkParams = Partial<Record<LinkParamsKeys, string | number>>;
|
|
||||||
|
|
||||||
export const linkPathBuilder = (
|
|
||||||
route: RoutePath,
|
|
||||||
params?: LinkParams,
|
|
||||||
lang?: Locale,
|
|
||||||
query?: Partial<Record<QueryParams, string | number | boolean>>,
|
|
||||||
) => {
|
|
||||||
let path = Paths[route]; // .replace(BasicPath, `/${lang}`);
|
|
||||||
if (params) {
|
|
||||||
Object.keys(params).forEach((key: unknown) => {
|
|
||||||
path = path.replace(`:${key}`, String(params[key as LinkParamsKeys]));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (query) {
|
|
||||||
path += `?${qs.stringify(query)}`;
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
.app {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Layout } from 'antd';
|
|
||||||
import { Switch, Route, Redirect } from 'react-router-dom';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
|
|
||||||
import Store from 'Store';
|
|
||||||
import { Paths } from './Paths';
|
|
||||||
|
|
||||||
import Dashboard from '../Dashboard';
|
|
||||||
import { Login, ForgotPassword } from '../Login';
|
|
||||||
import Sidebar from '../Sidebar';
|
|
||||||
import Header from '../Header';
|
|
||||||
import SetupGuide from '../SetupGuide';
|
|
||||||
import { GeneralSettings } from '../Settings';
|
|
||||||
|
|
||||||
import s from './Routes.module.pcss';
|
|
||||||
|
|
||||||
const { Content } = Layout;
|
|
||||||
|
|
||||||
const AuthRoutes: FC = React.memo(() => {
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={Paths.ForgotPassword}
|
|
||||||
component={ForgotPassword}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path={Paths.Login}
|
|
||||||
component={Login}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const AppRoutes: FC = observer(() => {
|
|
||||||
return (
|
|
||||||
<Layout className={s.app}>
|
|
||||||
<Sidebar />
|
|
||||||
<Layout>
|
|
||||||
<Header />
|
|
||||||
<Content>
|
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={Paths.Dashboard}
|
|
||||||
component={Dashboard}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={Paths.SetupGuide}
|
|
||||||
component={SetupGuide}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path={Paths.SettingsGeneral}
|
|
||||||
component={GeneralSettings}
|
|
||||||
/>
|
|
||||||
<Redirect to={Paths.Dashboard} />
|
|
||||||
</Switch>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const Routes: FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { login: { loggedIn } } = store;
|
|
||||||
if (loggedIn) {
|
|
||||||
return <AppRoutes />;
|
|
||||||
}
|
|
||||||
return <AuthRoutes />;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Routes;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Routes';
|
|
|
@ -1,52 +0,0 @@
|
||||||
import React, { FC, useContext, useEffect } from 'react';
|
|
||||||
import { Tabs, Grid } from 'antd';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
|
|
||||||
import { InnerLayout } from 'Common/ui';
|
|
||||||
import Store from 'Store';
|
|
||||||
|
|
||||||
import { General, QueryLog, Statistics, TAB_KEY } from './components';
|
|
||||||
|
|
||||||
const { useBreakpoint } = Grid;
|
|
||||||
const { TabPane } = Tabs;
|
|
||||||
|
|
||||||
const GeneralSettings: FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { ui: { intl }, generalSettings } = store;
|
|
||||||
const { inited } = generalSettings;
|
|
||||||
const screens = useBreakpoint();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!inited) {
|
|
||||||
generalSettings.init();
|
|
||||||
}
|
|
||||||
}, [inited]);
|
|
||||||
|
|
||||||
if (!inited) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabsPosition = screens.lg ? 'left' : 'top';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InnerLayout title={intl.getMessage('general_settings')}>
|
|
||||||
<Tabs
|
|
||||||
defaultActiveKey={TAB_KEY.GENERAL}
|
|
||||||
tabPosition={tabsPosition}
|
|
||||||
className="tabs"
|
|
||||||
>
|
|
||||||
<TabPane tab={intl.getMessage('filter_category_general')} key={TAB_KEY.GENERAL}>
|
|
||||||
<General/>
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tab={intl.getMessage('query_log_configuration')} key={TAB_KEY.QUERY_LOG}>
|
|
||||||
<QueryLog/>
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tab={intl.getMessage('statistics_configuration')} key={TAB_KEY.STATISTICS}>
|
|
||||||
<Statistics/>
|
|
||||||
</TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</InnerLayout>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default GeneralSettings;
|
|
|
@ -1,45 +0,0 @@
|
||||||
.title {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--gray900);
|
|
||||||
margin-bottom: 48px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio {
|
|
||||||
display: block;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
margin-top: -12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.save {
|
|
||||||
display: block;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nameTitle {
|
|
||||||
color: var(--black);
|
|
||||||
}
|
|
||||||
.nameDesc {
|
|
||||||
color: var(--gray700);
|
|
||||||
margin-right: 40px;
|
|
||||||
|
|
||||||
@media (--m-viewport) {
|
|
||||||
margin-right: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.select {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
margin-top: -12px;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
|
@ -1,169 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Button, Switch, Select } from 'antd';
|
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
|
|
||||||
import { Link } from 'Common/ui';
|
|
||||||
import Store from 'Store';
|
|
||||||
import { RoutePath } from 'Paths';
|
|
||||||
|
|
||||||
import { s } from '.';
|
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
const General: FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { ui: { intl }, generalSettings } = store;
|
|
||||||
const {
|
|
||||||
safebrowsing,
|
|
||||||
filteringConfig,
|
|
||||||
parental,
|
|
||||||
safesearch,
|
|
||||||
} = generalSettings;
|
|
||||||
|
|
||||||
const initialValues = {
|
|
||||||
...filteringConfig!.serialize(),
|
|
||||||
safebrowsing,
|
|
||||||
parental,
|
|
||||||
safesearch,
|
|
||||||
};
|
|
||||||
|
|
||||||
type InitialValues = typeof initialValues;
|
|
||||||
|
|
||||||
const onSubmit = async (values: InitialValues, helpers: FormikHelpers<InitialValues>) => {
|
|
||||||
// await generalSettings.updateQueryLogConfig(values);
|
|
||||||
if (initialValues.parental !== values.parental) {
|
|
||||||
generalSettings[values.parental ? 'parentalEnable' : 'parentalDisable']();
|
|
||||||
}
|
|
||||||
if (initialValues.safesearch !== values.safesearch) {
|
|
||||||
generalSettings[values.safesearch ? 'safebrowsingEnable' : 'safebrowsingDisable']();
|
|
||||||
}
|
|
||||||
if (initialValues.safebrowsing !== values.safebrowsing) {
|
|
||||||
generalSettings[values.safebrowsing ? 'safebrowsingEnable' : 'safebrowsingDisable']();
|
|
||||||
}
|
|
||||||
if (initialValues.enabled !== values.enabled
|
|
||||||
|| initialValues.interval !== values.interval) {
|
|
||||||
generalSettings.updateFilteringConfig({
|
|
||||||
interval: values.interval,
|
|
||||||
enabled: values.enabled,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
helpers.setSubmitting(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filtersLink = (e: string) => {
|
|
||||||
// TODO: fix link
|
|
||||||
return <Link to={RoutePath.Dashboard}>{e}</Link>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={s.title}>
|
|
||||||
{intl.getMessage('filter_category_general')}
|
|
||||||
</div>
|
|
||||||
<Formik
|
|
||||||
enableReinitialize
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({
|
|
||||||
handleSubmit,
|
|
||||||
values,
|
|
||||||
setFieldValue,
|
|
||||||
isSubmitting,
|
|
||||||
dirty,
|
|
||||||
}) => (
|
|
||||||
<form onSubmit={handleSubmit} noValidate className={s.form}>
|
|
||||||
<div className={s.item}>
|
|
||||||
<div>
|
|
||||||
<div className={s.nameTitle}>
|
|
||||||
{intl.getMessage('block_domain_use_filters_and_hosts')}
|
|
||||||
</div>
|
|
||||||
<div className={s.nameDesc}>
|
|
||||||
{intl.getMessage('filters_block_toggle_hint', { a: filtersLink })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Switch checked={values.enabled} onChange={(e) => setFieldValue('enabled', e)}/>
|
|
||||||
</div>
|
|
||||||
<div className={s.item}>
|
|
||||||
<div>
|
|
||||||
<div className={s.nameTitle}>
|
|
||||||
{intl.getMessage('filters_interval')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
value={values.interval}
|
|
||||||
onChange={(e) => setFieldValue('interval', e)}
|
|
||||||
className={s.select}
|
|
||||||
>
|
|
||||||
<Option value={0}>
|
|
||||||
{intl.getMessage('disabled')}
|
|
||||||
</Option>
|
|
||||||
<Option value={1}>
|
|
||||||
{intl.getPlural('interval_hours', 1, { count: 1 })}
|
|
||||||
</Option>
|
|
||||||
<Option value={12}>
|
|
||||||
{intl.getPlural('interval_hours', 12, { count: 12 })}
|
|
||||||
</Option>
|
|
||||||
<Option value={24}>
|
|
||||||
{intl.getPlural('interval_hours', 24, { count: 24 })}
|
|
||||||
</Option>
|
|
||||||
<Option value={72}>
|
|
||||||
{intl.getPlural('interval_days', 3, { count: 3 })}
|
|
||||||
</Option>
|
|
||||||
<Option value={168}>
|
|
||||||
{intl.getPlural('interval_days', 7, { count: 7 })}
|
|
||||||
</Option>
|
|
||||||
</Select>
|
|
||||||
<div className={s.item}>
|
|
||||||
<div>
|
|
||||||
<div className={s.nameTitle}>
|
|
||||||
{intl.getMessage('use_adguard_browsing_sec')}
|
|
||||||
</div>
|
|
||||||
<div className={s.nameDesc}>
|
|
||||||
{intl.getMessage('use_adguard_browsing_sec_hint')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Switch checked={values.safebrowsing} onChange={(e) => setFieldValue('safebrowsing', e)}/>
|
|
||||||
</div>
|
|
||||||
<div className={s.item}>
|
|
||||||
<div>
|
|
||||||
<div className={s.nameTitle}>
|
|
||||||
{intl.getMessage('use_adguard_parental')}
|
|
||||||
</div>
|
|
||||||
<div className={s.nameDesc}>
|
|
||||||
{intl.getMessage('use_adguard_parental_hint')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Switch checked={values.parental} onChange={(e) => setFieldValue('parental', e)}/>
|
|
||||||
</div>
|
|
||||||
<div className={s.item}>
|
|
||||||
<div>
|
|
||||||
<div className={s.nameTitle}>
|
|
||||||
{intl.getMessage('enforce_safe_search')}
|
|
||||||
</div>
|
|
||||||
<div className={s.nameDesc}>
|
|
||||||
{intl.getMessage('enforce_save_search_hint')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Switch checked={values.safesearch} onChange={(e) => setFieldValue('safesearch', e)}/>
|
|
||||||
</div>
|
|
||||||
{dirty && (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
className={s.save}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{intl.getMessage('save_btn')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default General;
|
|
|
@ -1,124 +0,0 @@
|
||||||
import React, { FC, useContext, useState } from 'react';
|
|
||||||
import { Radio, Button, Switch } from 'antd';
|
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
|
|
||||||
import { notifySuccess, ConfirmModalLayout } from 'Common/ui';
|
|
||||||
import { IQueryLogConfig } from 'Entities/QueryLogConfig';
|
|
||||||
import Store from 'Store';
|
|
||||||
|
|
||||||
import { s } from '.';
|
|
||||||
|
|
||||||
const { Group } = Radio;
|
|
||||||
|
|
||||||
const QueryLog: FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
|
||||||
const { ui: { intl }, generalSettings } = store;
|
|
||||||
const {
|
|
||||||
queryLogConfig,
|
|
||||||
} = generalSettings;
|
|
||||||
|
|
||||||
const onSubmit = async (values: IQueryLogConfig, helpers: FormikHelpers<IQueryLogConfig>) => {
|
|
||||||
await generalSettings.updateQueryLogConfig(values);
|
|
||||||
helpers.setSubmitting(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onReset = async () => {
|
|
||||||
const result = await generalSettings.querylogClear();
|
|
||||||
if (result) {
|
|
||||||
notifySuccess(intl.getMessage('query_log_cleared'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={s.title}>
|
|
||||||
{intl.getMessage('query_log_configuration')}
|
|
||||||
<Button onClick={() => setShowConfirm(true)}>
|
|
||||||
{intl.getMessage('query_log_clear')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<ConfirmModalLayout
|
|
||||||
visible={showConfirm}
|
|
||||||
onConfirm={onReset}
|
|
||||||
onClose={() => setShowConfirm(false)}
|
|
||||||
title={intl.getMessage('query_log_clear')}
|
|
||||||
buttonText={intl.getMessage('query_log_clear')}
|
|
||||||
>
|
|
||||||
{intl.getMessage('query_log_confirm_clear')}
|
|
||||||
</ConfirmModalLayout>
|
|
||||||
<Formik
|
|
||||||
enableReinitialize
|
|
||||||
initialValues={queryLogConfig!.serialize()}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({
|
|
||||||
handleSubmit,
|
|
||||||
values,
|
|
||||||
setFieldValue,
|
|
||||||
isSubmitting,
|
|
||||||
dirty,
|
|
||||||
}) => (
|
|
||||||
<form onSubmit={handleSubmit} noValidate className={s.form}>
|
|
||||||
<div className={s.item}>
|
|
||||||
<div>
|
|
||||||
<div className={s.nameTitle}>
|
|
||||||
{intl.getMessage('query_log_enable')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Switch checked={values.enabled} onChange={(e) => setFieldValue('enabled', e)}/>
|
|
||||||
</div>
|
|
||||||
<div className={s.item}>
|
|
||||||
<div>
|
|
||||||
<div className={s.nameTitle}>
|
|
||||||
{intl.getMessage('anonymize_client_ip')}
|
|
||||||
</div>
|
|
||||||
<div className={s.nameDesc}>
|
|
||||||
{intl.getMessage('anonymize_client_ip_desc')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Switch checked={values.anonymize_client_ip} onChange={(e) => setFieldValue('anonymize_client_ip', e)}/>
|
|
||||||
</div>
|
|
||||||
<div className={s.item}>
|
|
||||||
<div>
|
|
||||||
<div className={s.nameTitle}>
|
|
||||||
{intl.getMessage('query_log_retention')}
|
|
||||||
</div>
|
|
||||||
<div className={s.nameDesc}>
|
|
||||||
{intl.getMessage('query_log_retention_confirm')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Group value={values.interval} onChange={(e) => setFieldValue('interval', e.target.value)}>
|
|
||||||
<Radio value={1} className={s.radio}>
|
|
||||||
{intl.getMessage('interval_24_hour')}
|
|
||||||
</Radio>
|
|
||||||
<Radio value={7} className={s.radio}>
|
|
||||||
{intl.getPlural('interval_days', 7, { count: 7 })}
|
|
||||||
</Radio>
|
|
||||||
<Radio value={30} className={s.radio}>
|
|
||||||
{intl.getPlural('interval_days', 30, { count: 30 })}
|
|
||||||
</Radio>
|
|
||||||
<Radio value={90} className={s.radio}>
|
|
||||||
{intl.getPlural('interval_days', 90, { count: 90 })}
|
|
||||||
</Radio>
|
|
||||||
</Group>
|
|
||||||
{dirty && (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
className={s.save}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{intl.getMessage('save_btn')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default QueryLog;
|
|
|
@ -1,105 +0,0 @@
|
||||||
import React, { FC, useContext, useState } from 'react';
|
|
||||||
import { Radio, Button } from 'antd';
|
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
|
|
||||||
import { notifySuccess, ConfirmModalLayout } from 'Common/ui';
|
|
||||||
import { IStatsConfig } from 'Entities/StatsConfig';
|
|
||||||
import Store from 'Store';
|
|
||||||
|
|
||||||
import { s } from '.';
|
|
||||||
|
|
||||||
const { Group } = Radio;
|
|
||||||
|
|
||||||
const Statistics: FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
|
||||||
const { ui: { intl }, generalSettings } = store;
|
|
||||||
const {
|
|
||||||
statsConfig,
|
|
||||||
} = generalSettings;
|
|
||||||
|
|
||||||
const onSubmit = async (values: IStatsConfig, helpers: FormikHelpers<IStatsConfig>) => {
|
|
||||||
await generalSettings.updateStatsConfig(values);
|
|
||||||
helpers.setSubmitting(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onReset = async () => {
|
|
||||||
const result = await generalSettings.statsReset();
|
|
||||||
if (result) {
|
|
||||||
notifySuccess(intl.getMessage('stats_reset'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={s.title}>
|
|
||||||
{intl.getMessage('statistics_configuration')}
|
|
||||||
<Button onClick={() => setShowConfirm(true)}>
|
|
||||||
{intl.getMessage('statistics_clear')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<ConfirmModalLayout
|
|
||||||
visible={showConfirm}
|
|
||||||
onConfirm={onReset}
|
|
||||||
onClose={() => setShowConfirm(false)}
|
|
||||||
title={intl.getMessage('statistics_clear')}
|
|
||||||
buttonText={intl.getMessage('statistics_clear')}
|
|
||||||
>
|
|
||||||
{intl.getMessage('statistics_clear_confirm')}
|
|
||||||
</ConfirmModalLayout>
|
|
||||||
<Formik
|
|
||||||
enableReinitialize
|
|
||||||
initialValues={statsConfig!.serialize()}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({
|
|
||||||
handleSubmit,
|
|
||||||
values,
|
|
||||||
setFieldValue,
|
|
||||||
isSubmitting,
|
|
||||||
dirty,
|
|
||||||
}) => (
|
|
||||||
<form onSubmit={handleSubmit} noValidate>
|
|
||||||
<div className={s.item}>
|
|
||||||
<div>
|
|
||||||
<div className={s.nameTitle}>
|
|
||||||
{intl.getMessage('statistics_retention')}
|
|
||||||
</div>
|
|
||||||
<div className={s.nameDesc}>
|
|
||||||
{intl.getMessage('statistics_retention_desc')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Group value={values.interval} onChange={(e) => setFieldValue('interval', e.target.value)}>
|
|
||||||
<Radio value={1} className={s.radio}>
|
|
||||||
{intl.getMessage('interval_24_hour')}
|
|
||||||
</Radio>
|
|
||||||
<Radio value={7} className={s.radio}>
|
|
||||||
{intl.getPlural('interval_days', 7, { count: 7 })}
|
|
||||||
</Radio>
|
|
||||||
<Radio value={30} className={s.radio}>
|
|
||||||
{intl.getPlural('interval_days', 30, { count: 30 })}
|
|
||||||
</Radio>
|
|
||||||
<Radio value={90} className={s.radio}>
|
|
||||||
{intl.getPlural('interval_days', 90, { count: 90 })}
|
|
||||||
</Radio>
|
|
||||||
</Group>
|
|
||||||
{dirty && (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
className={s.save}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{intl.getMessage('save_btn')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Statistics;
|
|
|
@ -1,9 +0,0 @@
|
||||||
export { default as General } from './General';
|
|
||||||
export { default as QueryLog } from './QueryLog';
|
|
||||||
export { default as Statistics } from './Statistics';
|
|
||||||
export enum TAB_KEY {
|
|
||||||
GENERAL = 'GENERAL',
|
|
||||||
QUERY_LOG = 'QUERY_LOG',
|
|
||||||
STATISTICS = 'STATISTICS',
|
|
||||||
}
|
|
||||||
export { default as s } from './Common.module.pcss';
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as GeneralSettings } from './GeneralSettings';
|
|
|
@ -1 +0,0 @@
|
||||||
export { GeneralSettings } from './GeneralSettings';
|
|
|
@ -1,31 +0,0 @@
|
||||||
.title {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
@media (--m-viewport) {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--gray900);
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0 0 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.addresses {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address {
|
|
||||||
font-family: var(--font-family-monospace);
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
word-break: break-all;
|
|
||||||
color: var(--green400);
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Tabs, Grid } from 'antd';
|
|
||||||
|
|
||||||
import { InnerLayout } from 'Common/ui';
|
|
||||||
import { externalLink, p } from 'Common/formating';
|
|
||||||
import { DHCP_LINK } from 'Consts/common';
|
|
||||||
import Store from 'Store';
|
|
||||||
|
|
||||||
import s from './SetupGuide.module.pcss';
|
|
||||||
|
|
||||||
const { useBreakpoint } = Grid;
|
|
||||||
const { TabPane } = Tabs;
|
|
||||||
|
|
||||||
const SetupGuide: FC = () => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const { ui: { intl }, system } = store;
|
|
||||||
const screens = useBreakpoint();
|
|
||||||
const tabsPosition = screens.lg ? 'left' : 'top';
|
|
||||||
|
|
||||||
const { status } = system;
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
key: intl.getMessage('router'),
|
|
||||||
text: intl.getMessage('install_configure_router', { p }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Windows',
|
|
||||||
text: intl.getMessage('install_configure_windows', { p }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'macOS',
|
|
||||||
text: intl.getMessage('install_configure_macos', { p }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Linux',
|
|
||||||
text: intl.getMessage('install_configure_router', { p }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Android',
|
|
||||||
text: intl.getMessage('install_configure_android', { p }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'iOS',
|
|
||||||
text: intl.getMessage('install_configure_ios', { p }),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const addresses = (
|
|
||||||
<>
|
|
||||||
<div className={s.text}>
|
|
||||||
{intl.getMessage('install_configure_adresses')}
|
|
||||||
{status?.dnsAddresses && (
|
|
||||||
<div className={s.addresses}>
|
|
||||||
{status.dnsAddresses.map((address) => (
|
|
||||||
<div className={s.address} key={address}>
|
|
||||||
{address}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={s.text}>
|
|
||||||
{intl.getMessage('install_configure_dhcp', { dhcp: externalLink(DHCP_LINK) })}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InnerLayout title={intl.getMessage('setup_guide')}>
|
|
||||||
<Tabs
|
|
||||||
defaultActiveKey={intl.getMessage('router')}
|
|
||||||
tabPosition={tabsPosition}
|
|
||||||
className="tabs"
|
|
||||||
>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<TabPane tab={tab.key} key={tab.key}>
|
|
||||||
<div className={s.title}>
|
|
||||||
{intl.getMessage('install_configure_how_to_title', { value: tab.key })}
|
|
||||||
</div>
|
|
||||||
<div className={s.text}>
|
|
||||||
{tab.text}
|
|
||||||
</div>
|
|
||||||
{addresses}
|
|
||||||
</TabPane>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
</InnerLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SetupGuide;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './SetupGuide';
|
|
|
@ -1,23 +0,0 @@
|
||||||
.logo {
|
|
||||||
width: 118px;
|
|
||||||
height: 31px;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: calc(100% - 71px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout {
|
|
||||||
@media (--m-viewport) {
|
|
||||||
margin-top: auto!important;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Layout, Menu, Grid } from 'antd';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import { PieChartOutlined, FormOutlined, TableOutlined, ProfileOutlined, SettingOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
import Store from 'Store';
|
|
||||||
import { Link, Icon, Mask } from 'Common/ui';
|
|
||||||
import { RoutePath, linkPathBuilder } from 'Components/App/Routes/Paths';
|
|
||||||
|
|
||||||
import s from './Sidebar.module.pcss';
|
|
||||||
|
|
||||||
const { Sider } = Layout;
|
|
||||||
const { Item: MenuItem, SubMenu } = Menu;
|
|
||||||
const { useBreakpoint } = Grid;
|
|
||||||
|
|
||||||
const Sidebar: FC = observer(() => {
|
|
||||||
const store = useContext(Store);
|
|
||||||
const screens = useBreakpoint();
|
|
||||||
const { ui: { intl, sidebarOpen, toggleSidebar } } = store;
|
|
||||||
|
|
||||||
if (!Object.keys(screens).length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSidebar = () => {
|
|
||||||
if (!screens.xl) {
|
|
||||||
toggleSidebar();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Sider
|
|
||||||
collapsed={!sidebarOpen && !screens.xl}
|
|
||||||
collapsedWidth={0}
|
|
||||||
collapsible
|
|
||||||
onClick={handleSidebar}
|
|
||||||
className="sidebar"
|
|
||||||
trigger={null}
|
|
||||||
width={200}
|
|
||||||
>
|
|
||||||
<Icon icon="logo_light" className={s.logo} />
|
|
||||||
<Menu
|
|
||||||
mode="inline"
|
|
||||||
theme="dark"
|
|
||||||
className={s.menu}
|
|
||||||
>
|
|
||||||
<MenuItem key={linkPathBuilder(RoutePath.Dashboard)}>
|
|
||||||
<Link to={RoutePath.Dashboard}>
|
|
||||||
<PieChartOutlined className={s.icon} />
|
|
||||||
{intl.getMessage('dashboard')}
|
|
||||||
</Link>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key={linkPathBuilder(RoutePath.FiltersBlocklist)}>
|
|
||||||
<Link to={RoutePath.FiltersBlocklist}>
|
|
||||||
<FormOutlined className={s.icon} />
|
|
||||||
{intl.getMessage('filters')}
|
|
||||||
</Link>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key={linkPathBuilder(RoutePath.QueryLog)}>
|
|
||||||
<Link to={RoutePath.QueryLog}>
|
|
||||||
<TableOutlined className={s.icon} />
|
|
||||||
{intl.getMessage('query_log')}
|
|
||||||
</Link>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key={linkPathBuilder(RoutePath.SetupGuide)}>
|
|
||||||
<Link to={RoutePath.SetupGuide}>
|
|
||||||
<ProfileOutlined className={s.icon} />
|
|
||||||
{intl.getMessage('setup_guide')}
|
|
||||||
</Link>
|
|
||||||
</MenuItem>
|
|
||||||
<SubMenu
|
|
||||||
key="settings"
|
|
||||||
icon={<SettingOutlined className={s.icon} />}
|
|
||||||
title={intl.getMessage('settings')}
|
|
||||||
>
|
|
||||||
<Menu.Item key={linkPathBuilder(RoutePath.SettingsGeneral)}>
|
|
||||||
<Link to={RoutePath.SettingsGeneral}>
|
|
||||||
{intl.getMessage('general_settings')}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key={linkPathBuilder(RoutePath.SettingsDns)}>
|
|
||||||
<Link to={RoutePath.SettingsDns}>
|
|
||||||
{intl.getMessage('dns_settings')}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key={linkPathBuilder(RoutePath.SettingsEncryption)}>
|
|
||||||
<Link to={RoutePath.SettingsEncryption}>
|
|
||||||
{intl.getMessage('encryption_settings')}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key={linkPathBuilder(RoutePath.SettingsClients)}>
|
|
||||||
<Link to={RoutePath.SettingsClients}>
|
|
||||||
{intl.getMessage('client_settings')}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key={linkPathBuilder(RoutePath.SettingsDhcp)}>
|
|
||||||
<Link to={RoutePath.SettingsDhcp}>
|
|
||||||
{intl.getMessage('dhcp_settings')}
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
</SubMenu>
|
|
||||||
<MenuItem className={s.logout}>
|
|
||||||
<a href="control/logout">
|
|
||||||
<Icon icon="sign_out" className={s.icon} />
|
|
||||||
{intl.getMessage('sign_out')}
|
|
||||||
</a>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</Sider>
|
|
||||||
<Mask open={sidebarOpen} handle={handleSidebar} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Sidebar;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Sidebar';
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './App';
|
|
|
@ -1,122 +0,0 @@
|
||||||
import React, { FC } from 'react';
|
|
||||||
import { Layout } from 'antd';
|
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import cn from 'classnames';
|
|
||||||
|
|
||||||
import { IInitialConfigurationBeta } from 'Entities/InitialConfigurationBeta';
|
|
||||||
import Icons from 'Common/ui/Icons';
|
|
||||||
import {
|
|
||||||
DEFAULT_DNS_ADDRESS,
|
|
||||||
DEFAULT_DNS_PORT,
|
|
||||||
DEFAULT_IP_ADDRESS,
|
|
||||||
DEFAULT_IP_PORT,
|
|
||||||
} from 'Consts/install';
|
|
||||||
import { notifyError } from 'Common/ui';
|
|
||||||
import InstallStore from 'Store/stores/Install';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
|
|
||||||
import AdminInterface from './components/AdminInterface';
|
|
||||||
import Auth from './components/Auth';
|
|
||||||
import DnsServer from './components/DnsServer';
|
|
||||||
import Stepper from './components/Stepper';
|
|
||||||
import Welcome from './components/Welcome';
|
|
||||||
import ConfigureDevices from './components/ConfigureDevices';
|
|
||||||
|
|
||||||
const { Content } = Layout;
|
|
||||||
|
|
||||||
export type FormValues = IInitialConfigurationBeta & { step: number };
|
|
||||||
|
|
||||||
const InstallForm: FC = observer(() => {
|
|
||||||
const initialValues: FormValues = {
|
|
||||||
step: 0,
|
|
||||||
web: {
|
|
||||||
ip: [DEFAULT_IP_ADDRESS],
|
|
||||||
port: DEFAULT_IP_PORT,
|
|
||||||
},
|
|
||||||
dns: {
|
|
||||||
ip: [DEFAULT_DNS_ADDRESS],
|
|
||||||
port: DEFAULT_DNS_PORT,
|
|
||||||
},
|
|
||||||
password: '',
|
|
||||||
username: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNext = async (values: FormValues, { setFieldValue }: FormikHelpers<FormValues>) => {
|
|
||||||
const currentStep = values.step;
|
|
||||||
const checker = (condition: boolean, message: string) => {
|
|
||||||
if (condition) {
|
|
||||||
setFieldValue('step', currentStep + 1);
|
|
||||||
} else {
|
|
||||||
notifyError(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
switch (currentStep) {
|
|
||||||
case 1: {
|
|
||||||
// web
|
|
||||||
const check = await InstallStore.checkConfig(values);
|
|
||||||
checker(check?.web?.status === '', check?.web?.status || '');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
// dns
|
|
||||||
const check = await InstallStore.checkConfig(values);
|
|
||||||
checker(check?.dns?.status === '', check?.dns?.status || '');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 4: {
|
|
||||||
// configure
|
|
||||||
const config = await InstallStore.configure(values);
|
|
||||||
if (config) {
|
|
||||||
const { web } = values;
|
|
||||||
window.location.href = `http://${web.ip[0]}:${web.port}`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
setFieldValue('step', currentStep + 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={onNext}
|
|
||||||
>
|
|
||||||
{({ values, handleSubmit, setFieldValue }) => (
|
|
||||||
<form noValidate onSubmit={handleSubmit}>
|
|
||||||
<Stepper currentStep={values.step} />
|
|
||||||
{values.step === 0 && (
|
|
||||||
<Welcome onNext={() => setFieldValue('step', 1)}/>
|
|
||||||
)}
|
|
||||||
{values.step === 1 && (
|
|
||||||
<AdminInterface values={values} setFieldValue={setFieldValue} />
|
|
||||||
)}
|
|
||||||
{values.step === 2 && (
|
|
||||||
<Auth values={values} setFieldValue={setFieldValue} />
|
|
||||||
)}
|
|
||||||
{values.step === 3 && (
|
|
||||||
<DnsServer values={values} setFieldValue={setFieldValue} />
|
|
||||||
)}
|
|
||||||
{values.step === 4 && (
|
|
||||||
<ConfigureDevices values={values} setFieldValue={setFieldValue} />
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const Install: FC = () => {
|
|
||||||
return (
|
|
||||||
<Layout className={cn(theme.content.content, theme.content.content_auth)}>
|
|
||||||
<Content className={cn(theme.content.container, theme.content.container_auth)}>
|
|
||||||
<InstallForm />
|
|
||||||
</Content>
|
|
||||||
<Icons/>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Install;
|
|
|
@ -1,142 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import { FormikHelpers } from 'formik';
|
|
||||||
|
|
||||||
import { Input, Radio, Switch } from 'Common/controls';
|
|
||||||
import { DEFAULT_IP_ADDRESS } from 'Consts/install';
|
|
||||||
import { chechNetworkType, NETWORK_TYPE } from 'Helpers/installHelpers';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
import Store from 'Store/installStore';
|
|
||||||
|
|
||||||
import { FormValues } from '../../Install';
|
|
||||||
import StepButtons from '../StepButtons';
|
|
||||||
|
|
||||||
enum NETWORK_OPTIONS {
|
|
||||||
ALL = 'all',
|
|
||||||
CUSTOM = 'custom',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AdminInterfaceProps {
|
|
||||||
values: FormValues;
|
|
||||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const AdminInterface: FC<AdminInterfaceProps> = observer(({
|
|
||||||
values,
|
|
||||||
setFieldValue,
|
|
||||||
}) => {
|
|
||||||
const { ui: { intl }, install: { addresses } } = useContext(Store);
|
|
||||||
const { web: { ip } } = values;
|
|
||||||
const radioValue = ip.length === 1 && ip[0] === DEFAULT_IP_ADDRESS
|
|
||||||
? NETWORK_OPTIONS.ALL : NETWORK_OPTIONS.CUSTOM;
|
|
||||||
|
|
||||||
const onSelectRadio = (v: string | number) => {
|
|
||||||
const value = v === NETWORK_OPTIONS.ALL
|
|
||||||
? [DEFAULT_IP_ADDRESS] : [];
|
|
||||||
setFieldValue('web.ip', value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getManualBlock = () => (
|
|
||||||
<div className={theme.install.options}>
|
|
||||||
{addresses?.interfaces.map((a) => {
|
|
||||||
let name = '';
|
|
||||||
const type = chechNetworkType(a.name);
|
|
||||||
switch (type) {
|
|
||||||
case NETWORK_TYPE.ETHERNET:
|
|
||||||
name = `${intl.getMessage('ethernet')} (${a.name}) `;
|
|
||||||
break;
|
|
||||||
case NETWORK_TYPE.LOCAL:
|
|
||||||
name = `${intl.getMessage('localhost')} (${a.name}) `;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
name = a.name || '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div key={a.name}>
|
|
||||||
<div className={theme.install.name}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
{a.ipAddresses?.map((addrIp) => (
|
|
||||||
<div key={addrIp} className={theme.install.option}>
|
|
||||||
<div className={theme.install.address}>
|
|
||||||
http://{addrIp}
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
checked={values.web.ip.includes(addrIp)}
|
|
||||||
onChange={() => {
|
|
||||||
const temp = new Set(ip);
|
|
||||||
if (temp.has(addrIp)) {
|
|
||||||
temp.delete(addrIp);
|
|
||||||
} else {
|
|
||||||
temp.add(addrIp);
|
|
||||||
}
|
|
||||||
setFieldValue('web.ip', Array.from(temp.values()));
|
|
||||||
}}/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={theme.install.title}>
|
|
||||||
{intl.getMessage('install_admin_interface_title')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
|
||||||
{intl.getMessage('install_admin_interface_title_decs')}
|
|
||||||
</div>
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_admin_interface_where_interface')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_admin_interface_where_interface_desc')}
|
|
||||||
</div>
|
|
||||||
<Radio
|
|
||||||
value={radioValue}
|
|
||||||
onSelect={onSelectRadio}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
value: NETWORK_OPTIONS.ALL,
|
|
||||||
label: intl.getMessage('install_all_networks'),
|
|
||||||
desc: intl.getMessage('install_all_networks_description'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: NETWORK_OPTIONS.CUSTOM,
|
|
||||||
label: intl.getMessage('install_choose_networks'),
|
|
||||||
desc: intl.getMessage('install_choose_networks_desc'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{ radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()}
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_admin_interface_port')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_admin_interface_port_desc')}
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
label={`${intl.getMessage('port')}:`}
|
|
||||||
placeholder={intl.getMessage('port')}
|
|
||||||
type="number"
|
|
||||||
name="webPort"
|
|
||||||
value={values.web.port}
|
|
||||||
onChange={(v) => {
|
|
||||||
const port = v === '' ? '' : parseInt(v, 10);
|
|
||||||
setFieldValue('web.port', port);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<StepButtons
|
|
||||||
setFieldValue={setFieldValue}
|
|
||||||
currentStep={1}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default AdminInterface;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './AdminInterface';
|
|
|
@ -1,55 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import { FormikHelpers } from 'formik';
|
|
||||||
|
|
||||||
import { Input } from 'Common/controls';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
import Store from 'Store/installStore';
|
|
||||||
|
|
||||||
import StepButtons from '../StepButtons';
|
|
||||||
import { FormValues } from '../../Install';
|
|
||||||
|
|
||||||
interface AuthProps {
|
|
||||||
values: FormValues;
|
|
||||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const Auth: FC<AuthProps> = observer(({
|
|
||||||
values,
|
|
||||||
setFieldValue,
|
|
||||||
}) => {
|
|
||||||
const { ui: { intl } } = useContext(Store);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={theme.install.title}>
|
|
||||||
{intl.getMessage('install_auth_title')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
|
||||||
{intl.getMessage('install_auth_description')}
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
placeholder={intl.getMessage('login')}
|
|
||||||
type="username"
|
|
||||||
name="username"
|
|
||||||
value={values.username}
|
|
||||||
onChange={(v) => setFieldValue('username', v)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={intl.getMessage('password')}
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
value={values.password}
|
|
||||||
onChange={(v) => setFieldValue('password', v)}
|
|
||||||
/>
|
|
||||||
<StepButtons
|
|
||||||
setFieldValue={setFieldValue}
|
|
||||||
currentStep={2}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Auth;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Auth';
|
|
|
@ -1,142 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Tabs, Grid } from 'antd';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { FormikHelpers } from 'formik';
|
|
||||||
|
|
||||||
import { DHCP_LINK } from 'Consts/common';
|
|
||||||
import { danger, externalLink, p } from 'Common/formating';
|
|
||||||
import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/install';
|
|
||||||
import Store from 'Store/installStore';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
|
|
||||||
import { FormValues } from '../../Install';
|
|
||||||
import StepButtons from '../StepButtons';
|
|
||||||
|
|
||||||
const { useBreakpoint } = Grid;
|
|
||||||
const { TabPane } = Tabs;
|
|
||||||
|
|
||||||
interface ConfigureDevicesProps {
|
|
||||||
values: FormValues;
|
|
||||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigureDevices: FC<ConfigureDevicesProps> = ({
|
|
||||||
values, setFieldValue,
|
|
||||||
}) => {
|
|
||||||
const { ui: { intl }, install: { addresses } } = useContext(Store);
|
|
||||||
const screens = useBreakpoint();
|
|
||||||
const tabsPosition = screens.md ? 'left' : 'top';
|
|
||||||
|
|
||||||
const allIps = addresses?.interfaces.reduce<string[]>((all, data) => {
|
|
||||||
const { ipAddresses } = data;
|
|
||||||
if (ipAddresses) {
|
|
||||||
all.push(...ipAddresses);
|
|
||||||
}
|
|
||||||
return all;
|
|
||||||
}, [] as string[]);
|
|
||||||
|
|
||||||
const { web: { ip: webIp }, dns: { ip: dnsIp } } = values;
|
|
||||||
const selectedWebIps = webIp.length === 1 && webIp[0] === DEFAULT_IP_ADDRESS
|
|
||||||
? allIps : webIp;
|
|
||||||
const selectedDnsIps = dnsIp.length === 1 && dnsIp[0] === DEFAULT_IP_ADDRESS
|
|
||||||
? allIps : dnsIp;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={theme.install.title}>
|
|
||||||
{intl.getMessage('install_configure_title')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
|
||||||
{intl.getMessage('install_configure_danger_notice', { danger })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs defaultActiveKey="1" tabPosition={tabsPosition} className={theme.install.tabs}>
|
|
||||||
<TabPane tab={intl.getMessage('router')} key="1">
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_configure_how_to_title', { value: intl.getMessage('router') })}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_configure_router', { p })}
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tab="Windows" key="2">
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_configure_how_to_title', { value: 'Windows' })}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_configure_windows', { p })}
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tab="macOS" key="3">
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_configure_how_to_title', { value: 'macOS' })}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_configure_macos', { p })}
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tab="Linux" key="4">
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_configure_how_to_title', { value: 'Linux' })}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{/* TODO: add linux setup */}
|
|
||||||
{intl.getMessage('install_configure_router', { p })}
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tab="Android" key="5">
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_configure_how_to_title', { value: 'Android' })}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_configure_android', { p })}
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
|
||||||
<TabPane tab="iOS" key="6">
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_configure_how_to_title', { value: 'iOS' })}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_configure_ios', { p })}
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_configure_adresses')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_admin_interface_title')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{selectedWebIps?.map((ip) => (
|
|
||||||
<div key={ip} className={theme.install.ip}>
|
|
||||||
{ip}{values.web.port !== DEFAULT_IP_PORT && `:${values.web.port}`}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_dns_server_title')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{selectedDnsIps?.map((ip) => (
|
|
||||||
<div key={ip} className={theme.install.ip}>
|
|
||||||
{ip}{values.dns.port !== DEFAULT_DNS_PORT && `:${values.dns.port}`}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_configure_dhcp', { dhcp: externalLink(DHCP_LINK) })}
|
|
||||||
</div>
|
|
||||||
<StepButtons
|
|
||||||
setFieldValue={setFieldValue}
|
|
||||||
currentStep={4}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConfigureDevices;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './ConfigureDevices';
|
|
|
@ -1,142 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import { FormikHelpers } from 'formik';
|
|
||||||
|
|
||||||
import { Input, Radio, Switch } from 'Common/controls';
|
|
||||||
import { DEFAULT_IP_ADDRESS } from 'Consts/install';
|
|
||||||
import { chechNetworkType, NETWORK_TYPE } from 'Helpers/installHelpers';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
import Store from 'Store/installStore';
|
|
||||||
|
|
||||||
import { FormValues } from '../../Install';
|
|
||||||
import StepButtons from '../StepButtons';
|
|
||||||
|
|
||||||
enum NETWORK_OPTIONS {
|
|
||||||
ALL = 'all',
|
|
||||||
CUSTOM = 'custom',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DnsServerProps {
|
|
||||||
values: FormValues;
|
|
||||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const DnsServer: FC<DnsServerProps> = observer(({
|
|
||||||
values,
|
|
||||||
setFieldValue,
|
|
||||||
}) => {
|
|
||||||
const { ui: { intl }, install: { addresses } } = useContext(Store);
|
|
||||||
const { dns: { ip } } = values;
|
|
||||||
const radioValue = ip.length === 1 && ip[0] === DEFAULT_IP_ADDRESS
|
|
||||||
? NETWORK_OPTIONS.ALL : NETWORK_OPTIONS.CUSTOM;
|
|
||||||
|
|
||||||
const onSelectRadio = (v: string | number) => {
|
|
||||||
const value = v === NETWORK_OPTIONS.ALL
|
|
||||||
? [DEFAULT_IP_ADDRESS] : [];
|
|
||||||
setFieldValue('dns.ip', value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getManualBlock = () => (
|
|
||||||
<div className={theme.install.options}>
|
|
||||||
{addresses?.interfaces.map((a) => {
|
|
||||||
let name = '';
|
|
||||||
const type = chechNetworkType(a.name);
|
|
||||||
switch (type) {
|
|
||||||
case NETWORK_TYPE.ETHERNET:
|
|
||||||
name = `${intl.getMessage('ethernet')} (${a.name}) `;
|
|
||||||
break;
|
|
||||||
case NETWORK_TYPE.LOCAL:
|
|
||||||
name = `${intl.getMessage('localhost')} (${a.name}) `;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
name = a.name || '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div key={a.name}>
|
|
||||||
<div className={theme.install.name}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
{a.ipAddresses?.map((addrIp) => (
|
|
||||||
<div key={addrIp} className={theme.install.option}>
|
|
||||||
<div className={theme.install.address}>
|
|
||||||
{addrIp}
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
checked={values.dns.ip.includes(addrIp)}
|
|
||||||
onChange={() => {
|
|
||||||
const temp = new Set(ip);
|
|
||||||
if (temp.has(addrIp)) {
|
|
||||||
temp.delete(addrIp);
|
|
||||||
} else {
|
|
||||||
temp.add(addrIp);
|
|
||||||
}
|
|
||||||
setFieldValue('dns.ip', Array.from(temp.values()));
|
|
||||||
}}/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={theme.install.title}>
|
|
||||||
{intl.getMessage('install_dns_server_title')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
|
||||||
{intl.getMessage('install_dns_server_desc')}
|
|
||||||
</div>
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_dns_server_network_interfaces')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_dns_server_network_interfaces_desc')}
|
|
||||||
</div>
|
|
||||||
<Radio
|
|
||||||
value={radioValue}
|
|
||||||
onSelect={onSelectRadio}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
value: NETWORK_OPTIONS.ALL,
|
|
||||||
label: intl.getMessage('install_all_networks'),
|
|
||||||
desc: intl.getMessage('install_all_networks_description'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: NETWORK_OPTIONS.CUSTOM,
|
|
||||||
label: intl.getMessage('install_choose_networks'),
|
|
||||||
desc: intl.getMessage('install_choose_networks_desc'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{ radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()}
|
|
||||||
<div className={theme.install.subtitle}>
|
|
||||||
{intl.getMessage('install_dns_server_port')}
|
|
||||||
</div>
|
|
||||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
|
||||||
{intl.getMessage('install_dns_server_port_desc')}
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
label={`${intl.getMessage('port')}:`}
|
|
||||||
placeholder={intl.getMessage('port')}
|
|
||||||
type="number"
|
|
||||||
name="dnsPort"
|
|
||||||
value={values.dns.port}
|
|
||||||
onChange={(v) => {
|
|
||||||
const port = v === '' ? '' : parseInt(v, 10);
|
|
||||||
setFieldValue('dns.port', port);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<StepButtons
|
|
||||||
setFieldValue={setFieldValue}
|
|
||||||
currentStep={3}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default DnsServer;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './DnsServer';
|
|
|
@ -1,44 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import { FormikHelpers } from 'formik';
|
|
||||||
|
|
||||||
import Store from 'Store/installStore';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
|
|
||||||
import { FormValues } from '../../Install';
|
|
||||||
|
|
||||||
interface StepButtonsProps {
|
|
||||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
|
||||||
currentStep: number;
|
|
||||||
values: FormValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepButtons: FC<StepButtonsProps> = observer(({
|
|
||||||
setFieldValue,
|
|
||||||
currentStep,
|
|
||||||
}) => {
|
|
||||||
const { ui: { intl } } = useContext(Store);
|
|
||||||
return (
|
|
||||||
<div className={theme.install.actions}>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
type="ghost"
|
|
||||||
className={theme.install.button}
|
|
||||||
onClick={() => setFieldValue('step', currentStep - 1)}
|
|
||||||
>
|
|
||||||
{intl.getMessage('back')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
className={theme.install.button}
|
|
||||||
>
|
|
||||||
{intl.getMessage('next')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default StepButtons;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './StepButtons';
|
|
|
@ -1,66 +0,0 @@
|
||||||
.stepper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 16px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
|
|
||||||
@media (--m-viewport) {
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 7px;
|
|
||||||
width: 100%;
|
|
||||||
height: 1px;
|
|
||||||
background-color: var(--gray400);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
flex: 0;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current .circle {
|
|
||||||
transform: scale(2);
|
|
||||||
background-color: var(--green400);
|
|
||||||
border-color: var(--green400);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active .circle {
|
|
||||||
background-color: var(--green400);
|
|
||||||
border-color: var(--green400);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current:before,
|
|
||||||
&.active:before {
|
|
||||||
background-color: var(--green400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background-color: var(--white);
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid var(--gray400);
|
|
||||||
transition: var(--transition) transform, var(--transition) background, var(--transition) border;
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import React, { FC } from 'react';
|
|
||||||
import cn from 'classnames';
|
|
||||||
|
|
||||||
import s from './Stepper.module.pcss';
|
|
||||||
|
|
||||||
interface StepProps {
|
|
||||||
active: boolean;
|
|
||||||
current: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Step: FC<StepProps> = ({ active, current }) => {
|
|
||||||
return (
|
|
||||||
<div className={cn(s.wrap, { [s.active]: active, [s.current]: current })}>
|
|
||||||
<div className={s.circle} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface StepperProps {
|
|
||||||
currentStep: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Stepper: FC<StepperProps> = ({ currentStep }) => {
|
|
||||||
return (
|
|
||||||
<div className={s.stepper}>
|
|
||||||
<Step current={currentStep === 0} active={currentStep >= 0} />
|
|
||||||
<Step current={currentStep === 1} active={currentStep >= 1} />
|
|
||||||
<Step current={currentStep === 2} active={currentStep >= 2} />
|
|
||||||
<Step current={currentStep === 3} active={currentStep >= 3} />
|
|
||||||
<Step current={currentStep === 4} active={currentStep >= 4} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Stepper;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Stepper';
|
|
|
@ -1,38 +0,0 @@
|
||||||
import React, { FC, useContext } from 'react';
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
|
|
||||||
import Store from 'Store/installStore';
|
|
||||||
import Icon from 'Common/ui/Icon';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
|
|
||||||
interface WelcomeProps {
|
|
||||||
onNext: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Welcome: FC<WelcomeProps> = observer(({ onNext }) => {
|
|
||||||
const { ui: { intl } } = useContext(Store);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Icon icon="logo" className={theme.install.logo} />
|
|
||||||
<div className={theme.install.title}>
|
|
||||||
{intl.getMessage('install_wellcome_title')}
|
|
||||||
</div>
|
|
||||||
<div className={theme.install.text}>
|
|
||||||
{intl.getMessage('install_wellcome_desc')}
|
|
||||||
</div>
|
|
||||||
<div className={theme.install.actions}>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
type="primary"
|
|
||||||
className={theme.install.button}
|
|
||||||
onClick={onNext}
|
|
||||||
>
|
|
||||||
{intl.getMessage('install_wellcome_button')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Welcome;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Welcome';
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Install';
|
|
|
@ -1,67 +0,0 @@
|
||||||
import React, { FC, FocusEvent } from 'react';
|
|
||||||
import { Button as ButtonControl } from 'antd';
|
|
||||||
import cn from 'classnames';
|
|
||||||
|
|
||||||
type ButtonSize = 'small' | 'medium' | 'big';
|
|
||||||
type ButtonType = 'primary' | 'icon' | 'link' | 'outlined' | 'border' | 'ghost' | 'input' | 'edit';
|
|
||||||
type ButtonHTMLType = 'submit' | 'button' | 'reset';
|
|
||||||
type ButtonShape = 'circle' | 'round';
|
|
||||||
|
|
||||||
export interface ButtonProps {
|
|
||||||
className?: string;
|
|
||||||
danger?: boolean;
|
|
||||||
dataAttrs?: {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
disabled?: boolean;
|
|
||||||
htmlType?: ButtonHTMLType;
|
|
||||||
// icon?: IconType | 'dots_loader';
|
|
||||||
iconClassName?: string;
|
|
||||||
id?: string;
|
|
||||||
inGroup?: boolean;
|
|
||||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
||||||
onBlur?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
||||||
shape?: ButtonShape;
|
|
||||||
size?: ButtonSize;
|
|
||||||
type: ButtonType;
|
|
||||||
block?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button: FC<ButtonProps> = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
danger,
|
|
||||||
dataAttrs,
|
|
||||||
disabled,
|
|
||||||
htmlType,
|
|
||||||
// icon,
|
|
||||||
id,
|
|
||||||
onClick,
|
|
||||||
onBlur,
|
|
||||||
shape,
|
|
||||||
}) => {
|
|
||||||
const buttonClass = cn(
|
|
||||||
className,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ButtonControl
|
|
||||||
className={buttonClass}
|
|
||||||
danger={danger}
|
|
||||||
disabled={disabled}
|
|
||||||
{...dataAttrs}
|
|
||||||
htmlType={htmlType}
|
|
||||||
// icon={icon && (icon === 'dots_loader'
|
|
||||||
// ? <Dots className={iconClassName} />
|
|
||||||
// : <Icon icon={icon} className={iconClassName} />)}
|
|
||||||
id={id}
|
|
||||||
onClick={onClick}
|
|
||||||
onBlur={onBlur}
|
|
||||||
shape={shape}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ButtonControl>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Button;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './Button';
|
|
|
@ -1,146 +0,0 @@
|
||||||
import React, { FC, FocusEvent, KeyboardEvent, ClipboardEvent, ChangeEvent, useState } from 'react';
|
|
||||||
import { Input as InputControl } from 'antd';
|
|
||||||
import { InputProps as InputControlProps } from 'antd/lib/input';
|
|
||||||
import cn from 'classnames';
|
|
||||||
|
|
||||||
import { Icon } from 'Common/ui';
|
|
||||||
import theme from 'Lib/theme';
|
|
||||||
|
|
||||||
interface AdminInterfaceProps {
|
|
||||||
autoComplete?: InputControlProps['autoComplete'];
|
|
||||||
autoFocus?: InputControlProps['autoFocus'];
|
|
||||||
className?: string;
|
|
||||||
description?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
error?: boolean;
|
|
||||||
id?: string;
|
|
||||||
inputMode?: InputControlProps['inputMode'];
|
|
||||||
label?: string;
|
|
||||||
wrapperClassName?: string;
|
|
||||||
name: string;
|
|
||||||
onBlur?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
||||||
onChange?: (data: string, e?: ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
onFocus?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
||||||
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;
|
|
||||||
onPaste?: (e: ClipboardEvent<HTMLInputElement>) => void;
|
|
||||||
pattern?: InputControlProps['pattern'];
|
|
||||||
placeholder: string;
|
|
||||||
prefix?: InputControlProps['prefix'];
|
|
||||||
size?: InputControlProps['size'];
|
|
||||||
suffix?: InputControlProps['suffix'];
|
|
||||||
type: InputControlProps['type'];
|
|
||||||
value: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InputComponent: FC<AdminInterfaceProps> = ({
|
|
||||||
autoComplete,
|
|
||||||
autoFocus,
|
|
||||||
className,
|
|
||||||
description,
|
|
||||||
disabled,
|
|
||||||
error,
|
|
||||||
id,
|
|
||||||
inputMode,
|
|
||||||
label,
|
|
||||||
wrapperClassName,
|
|
||||||
name,
|
|
||||||
onBlur,
|
|
||||||
onChange,
|
|
||||||
onFocus,
|
|
||||||
onKeyDown,
|
|
||||||
onPaste,
|
|
||||||
pattern,
|
|
||||||
placeholder,
|
|
||||||
prefix,
|
|
||||||
size = 'middle',
|
|
||||||
suffix,
|
|
||||||
type,
|
|
||||||
value,
|
|
||||||
}) => {
|
|
||||||
const [inputType, setInputType] = useState(type);
|
|
||||||
|
|
||||||
const inputClass = cn(
|
|
||||||
'input',
|
|
||||||
{ input_big: size === 'large' },
|
|
||||||
{ input_medium: size === 'middle' },
|
|
||||||
{ input_small: size === 'small' },
|
|
||||||
className,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
|
|
||||||
if (onBlur) {
|
|
||||||
onBlur(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showPassword = () => {
|
|
||||||
if (inputType === 'password') {
|
|
||||||
setInputType('text');
|
|
||||||
} else {
|
|
||||||
setInputType('password');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showPasswordIcon = () => {
|
|
||||||
const icon = inputType === 'password' ? 'visibility_disable' : 'visibility_enable';
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
icon={icon}
|
|
||||||
className={theme.form.reveal}
|
|
||||||
onClick={showPassword}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validSuffix = (
|
|
||||||
<>
|
|
||||||
{!!suffix && suffix}
|
|
||||||
{(type === 'password') && showPasswordIcon()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
let descriptionView = null;
|
|
||||||
if (description) {
|
|
||||||
descriptionView = (
|
|
||||||
<div className={theme.form.label}>
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label htmlFor={id || name} className={cn(theme.form.group, wrapperClassName)}>
|
|
||||||
{label && (
|
|
||||||
<div className={theme.form.label}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<InputControl
|
|
||||||
autoComplete={autoComplete}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
className={inputClass}
|
|
||||||
disabled={disabled}
|
|
||||||
formNoValidate
|
|
||||||
id={id || name}
|
|
||||||
inputMode={inputMode}
|
|
||||||
name={name}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
onChange={(e) => onChange && onChange(e.target.value ? e.target.value : '', e)}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onPaste={onPaste}
|
|
||||||
pattern={pattern}
|
|
||||||
placeholder={placeholder}
|
|
||||||
prefix={prefix}
|
|
||||||
size="large"
|
|
||||||
suffix={validSuffix}
|
|
||||||
type={inputType}
|
|
||||||
value={value}
|
|
||||||
data-error={error}
|
|
||||||
/>
|
|
||||||
{descriptionView}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InputComponent;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as Input } from './Input';
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue