Pull request #961: New client dashboard
Merge in DNS/adguard-home from new-client-dashboard to master Squashed commit of the following: commit 7bbd67c1e3d2af62b96bf41bb356cd6b784e473e Merge: 113743a69cd9054c
Author: Vlad <v.abdulmyanov@adguard.com> Date: Wed Feb 3 16:01:17 2021 +0300 Merge branch 'master' into new-client-dashboard commit 113743a60665e40383d367dc17fa709dc54e4e2e Author: Vlad <v.abdulmyanov@adguard.com> Date: Wed Feb 3 15:45:16 2021 +0300 Remove unneded modal styles commit 04f9d93a9ac17ee046f0d5bedfb2bf5a5e6c0a48 Author: Vlad <v.abdulmyanov@adguard.com> Date: Wed Feb 3 14:19:56 2021 +0300 Consider comments commit 78a96cd8fed8b3e03547e7e45724c23db295f67b Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 18:46:52 2021 +0300 Remove old params for MiniCssExtractPlugin commit 40e5a9b2b1e04036deb70af17f2719eadd0c9c02 Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 18:27:46 2021 +0300 Fix mobile version commit 509cefc308f945b03cafa62bf48257490a0a4be1 Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 18:20:56 2021 +0300 Remove unneeded imports commit d192f39cd2503b8ec942f00ba78fca02cac9fa60 Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 18:20:13 2021 +0300 Finish first version of dashboard commit f82429e53d334874ff7dd0641092ec83c66ab61c Merge: fd91a0a33e0238aa
Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 17:12:59 2021 +0300 Merge branch 'master' into new-client-dashboard commit fd91a0a3d76c2a052a6548232b75d151d6065b88 Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 17:12:27 2021 +0300 wip commit 237679965052d38acfcd6a72d24b2444cc5b3896 Author: Vlad <v.abdulmyanov@adguard.com> Date: Fri Jan 29 11:18:10 2021 +0300 Finish general settings commit 397a7e10efd34a8d31bb175a5a5a7158338388d4 Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 28 19:24:03 2021 +0300 Add General settings page commit 486aaa6f3f9ad66f3a0dcfcccad9a32659767e90 Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 28 14:05:16 2021 +0300 Remove husky commit b895306c0655019ca56ce161e050d83b4e7f5ff1 Merge: a195f1f4154c9c1c
Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 28 14:03:37 2021 +0300 Merge branch 'master' into new-client-dashboard commit a195f1f4d46043d9c53dea08734733f9817b95a0 Merge: c45c5fe9 362f390f Author: Vlad <v.abdulmyanov@adguard.com> Date: Wed Jan 27 15:46:18 2021 +0300 Merge branch 'new-client-dashboard' of ssh://bit.adguard.com:7999/dns/adguard-home into new-client-dashboard commit c45c5fe92e6c5c852bec8f512dc46b4cd513156c Author: Vlad <v.abdulmyanov@adguard.com> Date: Wed Jan 27 15:46:01 2021 +0300 wip commit 362f390fd3dcfca75633a8d30a2e54c3c30b4f3d Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com> Date: Wed Jan 27 15:45:12 2021 +0300 Pull request #949: + client: add setup guide page Merge in DNS/adguard-home from 2554-setup-guide to new-client-dashboard Squashed commit of the following: commit c240d52e9e5d90429f2018fde808f4d04ccec138 Merge: 256f1056 137b88e4 Author: Ildar Kamalov <ik@adguard.com> Date: Wed Jan 27 14:13:52 2021 +0300 Merge branch 'new-client-dashboard' into 2554-setup-guide commit 256f1056770c67339e93275ab6dc7aaf2c10da0b Author: Ildar Kamalov <ik@adguard.com> Date: Wed Jan 27 14:10:45 2021 +0300 + client: add DNS addresses to the setup guide commit 0ecf91275a16ecc0dca23cae2ae209836fc622d2 Author: Ildar Kamalov <ik@adguard.com> Date: Wed Jan 27 14:00:12 2021 +0300 + client: add setup guide tabs commit 137b88e4253af5be32d542adbe74575ef74805c8 Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 21 19:17:58 2021 +0300 Add clients top commit c3318e6932d87fdff5f22d76bee12b49f099129a Merge: 2776276b 021eb22f Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 21 19:15:57 2021 +0300 Merge branch 'new-client-dashboard' of ssh://bit.adguard.com:7999/dns/adguard-home into new-client-dashboard commit 2776276b2e6dc026e1326b02c388fcf7d48d47ff Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 21 19:15:53 2021 +0300 Add top client info commit 021eb22ff877aec12eb7fab60147a2cc2ddd08b7 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 14:13:54 2021 +0300 Merge: client: add sidebar Squashed commit of the following: commit 6885ba953971e68602889fbb3219221f90265421 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 13:56:55 2021 +0300 add sidebar mask commit f069bfe8cba2b31355e19a51ca00bf774ee9e560 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 13:03:47 2021 +0300 fix store commit 77c8791002887ae022da07dc264d9010576e7bab Merge: d0a6eff6 ea6d54d4 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 13:01:04 2021 +0300 Merge branch 'new-client-dashboard' into 2254-sidebar commit d0a6eff67fd74533d63f5d56382085e98ddbb702 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 12:47:32 2021 +0300 client: remove unused file commit 9d2424477de85503fe41fa00cc1294cb0c0e7dfa Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 12:39:13 2021 +0300 client: header commit 9ddea19c136f15b184caa72d7e82738f7d4f3f1f Merge: 797f1248 b694bb05 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 10:57:24 2021 +0300 Merge branch 'new-client-dashboard' into 2254-sidebar commit 797f1248df5c1ef8e59c2a9999138f9e05a7adaa Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 10:51:57 2021 +0300 client: sidebar ... and 14 more commits
This commit is contained in:
parent
9cd9054cdb
commit
0c127039cf
|
@ -12,13 +12,10 @@
|
|||
"go:build": "cd .. && make REBUILD_CLIENT=0 build",
|
||||
"go:run": "sudo ../AdguardHome"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "yarn lint"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
@ -29,7 +26,8 @@
|
|||
"qs": "^6.9.4",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-router-dom": "^5.2.0"
|
||||
"react-router-dom": "^5.2.0",
|
||||
"recharts": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/classnames": "^2.2.10",
|
||||
|
@ -56,7 +54,6 @@
|
|||
"file-loader": "^6.1.1",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"http-proxy-middleware": "^1.0.6",
|
||||
"husky": "^4.3.0",
|
||||
"less": "^3.12.2",
|
||||
"less-loader": "^5.0.0",
|
||||
"mini-css-extract-plugin": "^1.1.1",
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as path from 'path';
|
|||
import * as morph from 'ts-morph';
|
||||
|
||||
import { ENT_DIR } from '../../consts';
|
||||
import { TYPES, toCamel, schemaParamParser } from './utils';
|
||||
import { TYPES, toCamel, schemaParamParser, uncapitalize } from './utils';
|
||||
|
||||
const { Project, QuoteKind } = morph;
|
||||
|
||||
|
@ -125,14 +125,37 @@ class EntitiesGenerator {
|
|||
'',
|
||||
]);
|
||||
|
||||
const { properties: sProps, required } = this.schemas[sName];
|
||||
|
||||
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 [
|
||||
|
@ -153,6 +176,23 @@ class EntitiesGenerator {
|
|||
),
|
||||
});
|
||||
});
|
||||
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 }[] = [];
|
||||
|
@ -310,7 +350,18 @@ class EntitiesGenerator {
|
|||
}
|
||||
}
|
||||
});
|
||||
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: [{
|
||||
|
@ -319,6 +370,20 @@ class EntitiesGenerator {
|
|||
}],
|
||||
});
|
||||
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
|
||||
|
@ -369,6 +434,7 @@ class EntitiesGenerator {
|
|||
w.writeLine('}');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// add serialize method;
|
||||
|
@ -378,6 +444,20 @@ class EntitiesGenerator {
|
|||
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) => {
|
||||
|
@ -442,6 +522,10 @@ class EntitiesGenerator {
|
|||
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);
|
||||
|
@ -502,7 +586,7 @@ class EntitiesGenerator {
|
|||
});
|
||||
update.addParameter({
|
||||
name: 'props',
|
||||
type: `Partial<I${sName}>`,
|
||||
type: additionalPropsOnly ? `I${sName}` : `Partial<I${sName}>`,
|
||||
});
|
||||
update.setBodyText((w) => { w.writeLine(`return new ${sName}({ ...this.serialize(), ...props });`); });
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@ const toCamel = (s: string) => {
|
|||
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',
|
||||
|
@ -37,7 +40,13 @@ const schemaParamParser = (schemaProp: any, openApi: any): [string, boolean, boo
|
|||
|
||||
type = `${temp[temp.length - 1]}`;
|
||||
|
||||
const cl = openApi ? openApi.components.schemas[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;
|
||||
|
@ -71,4 +80,4 @@ const schemaParamParser = (schemaProp: any, openApi: any): [string, boolean, boo
|
|||
return [type, isArray, isClass, isImport, isAdditional];
|
||||
};
|
||||
|
||||
export { TYPES, toCamel, capitalize, schemaParamParser };
|
||||
export { TYPES, toCamel, capitalize, uncapitalize, schemaParamParser };
|
||||
|
|
|
@ -7,10 +7,11 @@ 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: `http://${target.host}:${target.port}`, // target host
|
||||
target: devHost || `http://${target.host}:${target.port}`, // target host
|
||||
changeOrigin: true, // needed for virtual hosted sites
|
||||
};
|
||||
const apiProxy = proxy.createProxyMiddleware(options);
|
||||
|
|
|
@ -46,9 +46,6 @@ module.exports = merge(baseConfig, {
|
|||
},
|
||||
use: [{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
esModules: true,
|
||||
}
|
||||
}, 'css-loader', 'postcss-loader', {
|
||||
loader: 'less-loader',
|
||||
options: {
|
||||
|
@ -62,9 +59,6 @@ module.exports = merge(baseConfig, {
|
|||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
esModules: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import React, { FC, useContext } from 'react';
|
||||
import Store from 'Store';
|
||||
import Icons from 'Lib/theme/Icons';
|
||||
|
||||
const App: FC = observer(() => {
|
||||
const store = useContext(Store);
|
||||
return (
|
||||
<div>
|
||||
{store.ui.currentLang}
|
||||
<Icons/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,20 @@
|
|||
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;
|
|
@ -0,0 +1,136 @@
|
|||
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;
|
|
@ -0,0 +1,20 @@
|
|||
.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);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default as BlockCard } from './BlockCard';
|
|
@ -0,0 +1,16 @@
|
|||
.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;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default as BlockedQueries } from './BlockedQueries';
|
|
@ -0,0 +1,46 @@
|
|||
.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;
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default as ServerStatistics } from './ServerStatistics';
|
|
@ -0,0 +1,43 @@
|
|||
.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)
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default as TopClients } from './TopClients';
|
|
@ -0,0 +1,62 @@
|
|||
.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;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default as TopDomains } from './TopDomains';
|
|
@ -0,0 +1,5 @@
|
|||
export { BlockCard } from './BlockCard';
|
||||
export { TopClients } from './TopClients';
|
||||
export { TopDomains } from './TopDomains';
|
||||
export { BlockedQueries } from './BlockedQueries';
|
||||
export { ServerStatistics } from './ServerStatistics';
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Dashboard';
|
|
@ -0,0 +1,31 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
.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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default as ErrorBoundary } from './ErrorBoundary';
|
|
@ -0,0 +1,81 @@
|
|||
.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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Header';
|
|
@ -0,0 +1,65 @@
|
|||
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;
|
|
@ -0,0 +1,34 @@
|
|||
.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;
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Login } from './Login';
|
||||
export { default as ForgotPassword } from './ForgotPassword';
|
|
@ -0,0 +1,63 @@
|
|||
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;
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
.app {
|
||||
min-height: 100vh;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Routes';
|
|
@ -0,0 +1,52 @@
|
|||
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;
|
|
@ -0,0 +1,45 @@
|
|||
.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;
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
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;
|
|
@ -0,0 +1,124 @@
|
|||
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;
|
|
@ -0,0 +1,105 @@
|
|||
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;
|
|
@ -0,0 +1,9 @@
|
|||
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';
|
|
@ -0,0 +1 @@
|
|||
export { default as GeneralSettings } from './GeneralSettings';
|
|
@ -0,0 +1 @@
|
|||
export { GeneralSettings } from './GeneralSettings';
|
|
@ -0,0 +1,31 @@
|
|||
.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);
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SetupGuide';
|
|
@ -0,0 +1,23 @@
|
|||
.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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Sidebar';
|
|
@ -0,0 +1 @@
|
|||
export { default } from './App';
|
|
@ -2,9 +2,10 @@ 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 'Lib/theme/Icons';
|
||||
import Icons from 'Common/ui/Icons';
|
||||
import {
|
||||
DEFAULT_DNS_ADDRESS,
|
||||
DEFAULT_DNS_PORT,
|
||||
|
@ -109,8 +110,8 @@ const InstallForm: FC = observer(() => {
|
|||
|
||||
const Install: FC = () => {
|
||||
return (
|
||||
<Layout className={theme.install.layout}>
|
||||
<Content className={theme.install.container}>
|
||||
<Layout className={cn(theme.content.content, theme.content.content_auth)}>
|
||||
<Content className={cn(theme.content.container, theme.content.container_auth)}>
|
||||
<InstallForm />
|
||||
</Content>
|
||||
<Icons/>
|
||||
|
|
|
@ -3,10 +3,11 @@ 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 { danger, p } from 'Common/formating';
|
||||
import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/install';
|
||||
|
||||
import { FormValues } from '../../Install';
|
||||
import StepButtons from '../StepButtons';
|
||||
|
@ -26,17 +27,6 @@ const ConfigureDevices: FC<ConfigureDevicesProps> = ({
|
|||
const screens = useBreakpoint();
|
||||
const tabsPosition = screens.md ? 'left' : 'top';
|
||||
|
||||
const dhcp = (e: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={theme.link.link}
|
||||
>
|
||||
{e}
|
||||
</a>
|
||||
);
|
||||
|
||||
const allIps = addresses?.interfaces.reduce<string[]>((all, data) => {
|
||||
const { ipAddresses } = data;
|
||||
if (ipAddresses) {
|
||||
|
@ -138,7 +128,7 @@ const ConfigureDevices: FC<ConfigureDevicesProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_configure_dhcp', { dhcp })}
|
||||
{intl.getMessage('install_configure_dhcp', { dhcp: externalLink(DHCP_LINK) })}
|
||||
</div>
|
||||
<StepButtons
|
||||
setFieldValue={setFieldValue}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Button';
|
|
@ -6,7 +6,7 @@ import s from './Radio.module.pcss';
|
|||
|
||||
const { Group } = Radio;
|
||||
|
||||
interface AdminInterfaceProps {
|
||||
interface RadioProps {
|
||||
options: {
|
||||
label: string;
|
||||
desc?: string;
|
||||
|
@ -16,7 +16,7 @@ interface AdminInterfaceProps {
|
|||
value: string | number;
|
||||
}
|
||||
|
||||
const RadioComponent: FC<AdminInterfaceProps> = observer(({
|
||||
const RadioComponent: FC<RadioProps> = observer(({
|
||||
options, onSelect, value,
|
||||
}) => {
|
||||
if (options.length === 0) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default as Radio } from './Radio';
|
||||
export { Input } from './Input';
|
||||
export { Switch } from './Switch';
|
||||
export { default as Button } from './Button';
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import theme from 'Lib/theme';
|
||||
|
||||
const code = (e: string) => {
|
||||
return (
|
||||
<code className={theme.text.code}>
|
||||
{e}
|
||||
</code>
|
||||
);
|
||||
};
|
||||
|
||||
export default code;
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import theme from 'Lib/theme';
|
||||
|
||||
export const externalLink = (link: string) => (e: string) => (
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={theme.link.link}
|
||||
>
|
||||
{e}
|
||||
</a>
|
||||
);
|
|
@ -1,2 +1,4 @@
|
|||
export { default as danger } from './danger';
|
||||
export { default as p } from './p';
|
||||
export { default as code } from './code';
|
||||
export { externalLink } from './externalLink';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { IconType } from 'Lib/theme/Icons';
|
||||
import { IconType } from 'Common/ui/Icons';
|
||||
|
||||
import s from './Icon.module.pcss';
|
||||
|
||||
|
@ -22,4 +22,4 @@ const Icon: FC<IconProps> = ({ icon, color, className, onClick }) => {
|
|||
};
|
||||
|
||||
export default Icon;
|
||||
export { IconType } from 'Lib/theme/Icons';
|
||||
export { IconType } from 'Common/ui/Icons';
|
||||
|
|
|
@ -4,7 +4,13 @@ import './Icon.pcss';
|
|||
export type IconType =
|
||||
'logo' |
|
||||
'visibility_disable' |
|
||||
'visibility_enable';
|
||||
'visibility_enable' |
|
||||
'logo_shield' |
|
||||
'logo_light' |
|
||||
'sign_out' |
|
||||
'user' |
|
||||
'language' |
|
||||
'close_big';
|
||||
|
||||
const Icons: FC = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="icons">
|
||||
|
@ -18,6 +24,16 @@ const Icons: FC = () => (
|
|||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="logo_light" viewBox="0 0 398 100">
|
||||
<path d="M127.772 92V72.4h2.212v8.708h11.312V72.4h2.212V92h-2.212v-8.82h-11.312V92h-2.212zm37.976-2.632c-1.885343 1.9786766-4.283985 2.968-7.196 2.968-2.912015 0-5.301324-.9893234-7.168-2.968s-2.8-4.367986-2.8-7.168c0-2.7813472.942657-5.1659901 2.828-7.154 1.885343-1.9880099 4.283985-2.982 7.196-2.982 2.912015 0 5.301324.9893234 7.168 2.968s2.8 4.367986 2.8 7.168c0 2.7813472-.942657 5.1706567-2.828 7.168zm-12.684-1.428c1.474674 1.5680078 3.322656 2.352 5.544 2.352 2.221344 0 4.055326-.7793255 5.502-2.338 1.446674-1.5586745 2.17-3.4766553 2.17-5.754 0-2.258678-.732659-4.1719922-2.198-5.74-1.465341-1.5680078-3.308656-2.352-5.53-2.352s-4.055326.7793255-5.502 2.338c-1.446674 1.5586745-2.17 3.4766553-2.17 5.754 0 2.258678.727993 4.1719922 2.184 5.74zM173.652 92V72.4h2.24l7.14 10.696 7.14-10.696h2.24V92H190.2V76.124l-7.14 10.5h-.112l-7.14-10.472V92h-2.156zm24.704 0V72.4h14.168v2.016h-11.956v6.692h10.696v2.016h-10.696v6.86h12.096V92h-14.308z" fill="#FFF"/>
|
||||
<path d="M49.2867362 0C33.8812166 0 15.2983087 3.57659574 0 11.4489362 0 28.4510638-.2111543 70.8085106 49.2867362 99.75 98.7857208 70.8085106 98.575653 28.4510638 98.575653 11.4489362 83.2762578 3.57659574 64.6933498 0 49.2867362 0z" fill="#68BC71"/>
|
||||
<path d="M49.236383 99.7205453C-.21101859 70.7797691 0 28.4452847 0 11.4489234 15.2816399 3.58515676 33.8407358.0077829 49.236383 0v99.7205453z" fill="#67B279"/>
|
||||
<path d="M47.4889507 66.5564478l29.8045071-39.6581001c-2.1840137-1.728257-4.0997057-.508489-5.1542723.4358476l-.0384803.0030267-24.850956 25.52231-9.3631787-11.1242041c-4.4668281-5.0949782-10.5394262-1.20867-11.9579951-.1816031l21.5603753 25.002723" fill="#FFF"/>
|
||||
<g transform="translate(125 18)" fill="#FFF">
|
||||
<path d="M0 37.3701657L15.8591452.36740331h7.506662L39.2249524 37.3701657h-8.5110746l-3.3832843-8.2403314H11.6829036l-3.38328429 8.2403314H0zm14.5904136-15.378453h9.83267l-4.916335-11.9143646-4.916335 11.9143646zm28.8831401 15.378453V.62983425h14.4318221c5.7798062 0 10.50226 1.74077449 14.167503 5.22237569C75.7381218 9.33381115 77.5707158 13.716364 77.5707158 19c0 5.248645-1.8414045 9.6224678-5.5242689 13.121547-3.6828643 3.4990792-8.3965076 5.2486187-14.1410711 5.2486187H43.4735537zm8.1410278-7.2955801h6.2907943c3.3480585 0 6.0440964-1.0234704 8.088164-3.070442C68.0376176 24.9571721 69.0596412 22.2891509 69.0596412 19c0-3.2541599-1.0308341-5.9134335-3.0925333-7.9779006-2.0616992-2.064467-4.7489163-3.09668504-8.0617321-3.09668504h-6.2907943V30.0745856zM101.167474 38c-5.7445633 0-10.4229644-1.7845125-14.0353433-5.3535912C83.5197518 29.0773302 81.7135894 24.5285728 81.7135894 19c0-5.283636 1.8502151-9.77116018 5.5507008-13.46270718C90.964776 1.84574581 95.5815032 0 101.11461 0c3.207088 0 5.920737.4111377 8.141028 1.23342541 2.220292.82228773 4.352444 2.09069125 6.396522 3.80524862l-5.12779 6.14088397c-1.55068-1.29466576-3.048473-2.2394077-4.493425-2.83425413-1.444951-.59484644-3.171829-.8922652-5.180654-.8922652-2.9603883 0-5.4713945 1.12844176-7.5330937 3.38535913C91.2554981 13.0953152 90.224664 15.815822 90.224664 19c0 3.3591328 1.0484552 6.140873 3.1453971 8.3453039 2.0969419 2.2044309 4.7841591 3.3066298 8.0617319 3.3066298 3.030874 0 5.585933-.7347993 7.665254-2.2044199V23.198895h-8.193892v-6.980663h16.070601v15.9558011C112.356959 36.0580305 107.088251 38 101.167474 38zm39.066361-.0524862c-5.039709 0-8.969228-1.3908701-11.788631-4.1726519-2.819418-2.7817819-4.229105-6.8319255-4.229105-12.1505525V.62983425h8.141027V21.4143646c0 2.9392413.696034 5.1873772 2.088121 6.7444752 1.392088 1.557098 3.35684 2.3356353 5.894316 2.3356353s4.502228-.7522945 5.894315-2.256906c1.392088-1.5046116 2.088121-3.6915142 2.088121-6.5607735V.62983425h8.141028V21.3618785c0 5.4585908-1.436119 9.5874629-4.308401 12.3867403-2.872282 2.7992773-6.845839 4.198895-11.920791 4.198895zm18.4356-.5773481L174.52858.36740331h7.506663l15.859145 37.00276239h-8.511075l-3.383284-8.2403314h-15.64769l-3.383284 8.2403314h-8.29962zm14.590414-15.378453h9.83267l-4.916335-11.9143646-4.916335 11.9143646zm28.88314 15.378453V.62983425h16.916422c4.687281 0 8.281985 1.24216069 10.784218 3.72651934 2.114563 2.09945801 3.171829 4.93368381 3.171829 8.50276241 0 5.6335457-2.643164 9.4300086-7.929572 11.3895028l9.039712 13.1215469h-9.515487l-8.0353-11.756906h-6.290794v11.756906h-8.141028zm8.141028-18.8950276h8.246755c1.973593 0 3.506628-.4811186 4.599152-1.4433701 1.092525-.9622516 1.638779-2.2481504 1.638779-3.8577349 0-1.7145573-.563875-3.0179513-1.691642-3.91022095-1.127767-.89226965-2.696045-1.33839779-4.70488-1.33839779h-8.088164V18.4751381zm28.618821 18.8950276V.62983425h14.431822c5.779806 0 10.50226 1.74077449 14.167503 5.22237569C271.167406 9.33381115 273 13.716364 273 19c0 5.248645-1.841405 9.6224678-5.524269 13.121547-3.682864 3.4990792-8.396507 5.2486187-14.141071 5.2486187h-14.431822zm8.141028-7.2955801h6.290794c3.348058 0 6.044096-1.0234704 8.088164-3.070442 2.044078-2.0469715 3.066101-4.7149927 3.066101-8.0041436 0-3.2541599-1.030834-5.9134335-3.092533-7.9779006-2.061699-2.064467-4.748916-3.09668504-8.061732-3.09668504h-6.290794V30.0745856z" />
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="visibility_disable" viewBox="0 0 24 24" fill="currentColor" fillRule="evenodd" clipRule="evenodd">
|
||||
<path d="M6.07675 11.0186L5.30088 11.4665C4.88614 11.706 4.35582 11.5639 4.11638 11.1491C3.87693 10.7344 4.01903 10.2041 4.43376 9.96464L5.77791 9.1886C5.82632 9.16065 5.87632 9.1379 5.92724 9.12017C5.94 9.11267 5.95302 9.10545 5.96629 9.09852C6.39087 8.877 6.91464 9.04161 7.13616 9.4662C7.63369 10.4198 9.41088 12.43 12.3523 12.4681C15.2937 12.43 17.0709 10.4198 17.5684 9.4662C17.7899 9.04161 18.3137 8.877 18.7383 9.09852C18.7844 9.1226 18.8275 9.15025 18.8674 9.18096C18.8719 9.18347 18.8764 9.18601 18.8809 9.1886L20.225 9.96464C20.6398 10.2041 20.7818 10.7344 20.5424 11.1491C20.303 11.5639 19.7726 11.706 19.3579 11.4665L18.614 11.037C18.188 11.6053 17.575 12.2431 16.7787 12.7966L17.2222 13.5647C17.4616 13.9794 17.3195 14.5097 16.9048 14.7492C16.4901 14.9886 15.9597 14.8465 15.7203 14.4318L15.2549 13.6258C14.6462 13.8742 13.9706 14.0595 13.2289 14.1469V15.1327C13.2289 15.6116 12.8407 15.9998 12.3618 15.9998C11.8829 15.9998 11.4947 15.6116 11.4947 15.1327V14.1492C10.607 14.0466 9.81358 13.804 9.11589 13.4803L8.56656 14.4318C8.32711 14.8465 7.79679 14.9886 7.38206 14.7492C6.96732 14.5097 6.82523 13.9794 7.06467 13.5647L7.63183 12.5823C6.969 12.0763 6.44978 11.5196 6.07675 11.0186Z" />
|
||||
</symbol>
|
||||
|
@ -26,6 +42,42 @@ const Icons: FC = () => (
|
|||
<path fillRule="evenodd" clipRule="evenodd" d="M4 11.9999C4.02485 11.6762 4.15136 11.3586 4.37852 11.0961L4.37907 11.0955C4.47595 10.9837 5.34608 9.99479 6.66752 9.0233C7.95858 8.07415 9.87032 7 12.0213 7C14.1723 7 16.084 8.07415 17.3751 9.0233C18.6965 9.99479 19.5666 10.9837 19.6635 11.0955L19.6676 11.1003C19.8904 11.3598 20.0171 11.6759 20.0422 11.9999C20.0171 12.324 19.8904 12.6402 19.6676 12.8997L19.6635 12.9045C19.5666 13.0163 18.6965 14.0052 17.3751 14.9767C16.084 15.9259 14.1723 17 12.0213 17C9.87032 17 7.95858 15.9259 6.66752 14.9767C5.34608 14.0052 4.47595 13.0163 4.37907 12.9045L4.37852 12.9039C4.15136 12.6414 4.02485 12.3237 4 11.9999ZM18.6435 11.9425C18.6588 11.9603 18.6715 11.9796 18.6815 11.9999C18.6715 12.0203 18.6588 12.0397 18.6435 12.0575C18.5147 12.2061 15.455 15.6908 12.0213 15.6908C8.58758 15.6908 5.52785 12.2061 5.39911 12.0575C5.38362 12.0397 5.37086 12.0202 5.36082 11.9999C5.37086 11.9797 5.38362 11.9603 5.39911 11.9425C5.52785 11.7939 8.58758 8.30924 12.0213 8.30924C15.455 8.30924 18.5147 11.7939 18.6435 11.9425Z" />
|
||||
<circle cx="12" cy="11" r="3" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="logo_shield" viewBox="0 0 24 24">
|
||||
<g fill="none">
|
||||
<path fill="#68BC71" d="M11.6126463,0 C7.98288984,0 3.6044961,0.860534313 0,2.75463127 C0,6.84536873 -0.0497509133,17.0366341 11.6126463,24 C23.2753014,17.0366341 23.2258065,6.84536873 23.2258065,2.75463127 C19.6210544,0.860534313 15.2426606,0 11.6126463,0 L11.6126463,0 Z"/>
|
||||
<path fill="#67B279" d="M11.6129032,24 C-0.0497708865,17.034749 0,6.8459998 0,2.75544183 C3.60433067,0.862848894 7.98168277,0.00187312864 11.6129032,0 L11.6129032,24 L11.6129032,24 Z"/>
|
||||
<path fill="#FFF" d="M11.393024,16.2580645 L18.5806452,6.40983016 C18.0539509,5.98065478 17.5919648,6.28355787 17.3376467,6.51806351 L17.3283668,6.51881513 L11.3353385,12.8567307 L9.07732492,10.0942744 C8.00010972,8.82904637 6.53564885,9.79412725 6.19354839,10.0491772 L11.393024,16.2580645"/>
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="sign_out" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M15.5555985,7 L20,12 M15.5555985,17 L20,12 L8.80095387,12 M5,4 L5,20 L11,20 M5,20 L5,4 L11,4" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="user" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<circle cx="12" cy="12" r="12" fill="#D8D8D8"/>
|
||||
<g transform="translate(3 3)">
|
||||
<rect width="18" height="18" fill="#000" fillRule="nonzero" opacity="0"/>
|
||||
<path fill="#888" d="M15.0908203,13.4226563 C14.7585938,12.6351563 14.2804687,11.9285156 13.6740234,11.3220703 C13.0675781,10.715625 12.3609375,10.2392578 11.5734375,9.90527344 C11.5664062,9.90175781 11.559375,9.9 11.5523437,9.89648438 C12.6474609,9.10546875 13.359375,7.81699219 13.359375,6.36328125 C13.359375,3.95507812 11.4082031,2.00390625 9,2.00390625 C6.59179687,2.00390625 4.640625,3.95507812 4.640625,6.36328125 C4.640625,7.81699219 5.35253906,9.10546875 6.44765625,9.89824219 C6.440625,9.90175781 6.43359375,9.90351563 6.4265625,9.90703125 C5.6390625,10.2392578 4.93242187,10.715625 4.32597656,11.3238281 C3.71953125,11.9302734 3.24316406,12.6369141 2.90917969,13.4244141 C2.58222656,14.1943359 2.40820312,15.0117188 2.39058925,15.8519531 C2.38886719,15.9310547 2.45214844,15.9960938 2.53125,15.9960938 L3.5859375,15.9960938 C3.66328125,15.9960938 3.72480469,15.9345703 3.7265625,15.8589844 C3.76171875,14.5019531 4.30664062,13.2310547 5.26992187,12.2677734 C6.26660156,11.2710938 7.59023437,10.7226563 9,10.7226563 C10.4097656,10.7226563 11.7333984,11.2710938 12.7300781,12.2677734 C13.6933594,13.2310547 14.2382813,14.5019531 14.2734375,15.8589844 C14.2751953,15.9363281 14.3367188,15.9960938 14.4140625,15.9960938 L15.46875,15.9960938 C15.5478516,15.9960938 15.6111328,15.9310547 15.6094108,15.8519531 C15.5917969,15.0117188 15.4177734,14.1943359 15.0908203,13.4226563 Z M9,9.38671875 C8.19316406,9.38671875 7.43378906,9.07207031 6.8625,8.50078125 C6.29121094,7.92949219 5.9765625,7.17011719 5.9765625,6.36328125 C5.9765625,5.55644531 6.29121094,4.79707031 6.8625,4.22578125 C7.43378906,3.65449219 8.19316406,3.33984375 9,3.33984375 C9.80683594,3.33984375 10.5662109,3.65449219 11.1375,4.22578125 C11.7087891,4.79707031 12.0234375,5.55644531 12.0234375,6.36328125 C12.0234375,7.17011719 11.7087891,7.92949219 11.1375,8.50078125 C10.5662109,9.07207031 9.80683594,9.38671875 9,9.38671875 Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="language" width="24" height="24" viewBox="0 0 19 18">
|
||||
<g fill="none" fillRule="evenodd" stroke="#888">
|
||||
<path d="M9.00703675,0.5 C11.0723523,0.5 12.9657989,1.23535701 14.4387791,2.45872525 C12.8188262,4.16233424 11.8254187,6.46525815 11.8254187,9 C11.8254187,11.5350474 12.8190766,13.8382185 14.4381487,15.5418354 C12.9654155,16.7648001 11.072137,17.5 9.00703675,17.5 C6.65783869,17.5 4.53102141,16.548573 2.99151519,15.0102695 C1.45215046,13.4721074 0.5,11.3471655 0.5,9 C0.5,6.65283448 1.45215046,4.5278926 2.99151519,2.98973049 C4.53102141,1.45142701 6.65783869,0.5 9.00703675,0.5 Z"/>
|
||||
<circle cx="9" cy="9" r="8.5"/>
|
||||
<path d="M9.16270935,0.5 C11.5119074,0.5 13.6387247,1.45142701 15.1782309,2.98973049 C16.7175956,4.5278926 17.6697461,6.65283448 17.6697461,9 C17.6697461,11.3471655 16.7175956,13.4721074 15.1782309,15.0102695 C13.6387247,16.548573 11.5119074,17.5 9.16270935,17.5 C7.09739383,17.5 5.20394722,16.764643 3.73094583,15.5413114 C5.35085425,13.8378107 6.34432739,11.5348228 6.34432739,9 C6.34432739,6.46487607 5.35060951,4.16164247 3.73144024,2.4580788 C5.20429995,1.23521198 7.0975914,0.5 9.16270935,0.5 Z"/>
|
||||
<line x1="9" x2="9" y1="1" y2="17" strokeLinecap="square"/>
|
||||
<line x1="1" x2="17" y1="9" y2="9" strokeLinecap="square"/>
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
<symbol id="close_big" viewBox="0 0 24 24" fill="currentColor" fillRule="evenodd" clipRule="evenodd">
|
||||
<path d="M6.248 4.48L4.834 5.894l5.48 5.48-5.834 5.834 1.414 1.414 5.834-5.834 5.834 5.834 1.414-1.415-5.834-5.833 5.48-5.48-1.414-1.414-5.48 5.48-5.48-5.48z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 22px;
|
||||
margin-right: 10px;
|
||||
color: var(--gray700);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import React, { FC, useContext } from 'react';
|
||||
|
||||
import { Icon } from 'Common/ui';
|
||||
import Store from 'Store';
|
||||
import { LANGUAGES } from 'Localization';
|
||||
|
||||
import s from './LangSelect.module.pcss';
|
||||
|
||||
const LangSelector: FC = () => {
|
||||
const store = useContext(Store);
|
||||
const { ui: { currentLang } } = store;
|
||||
|
||||
const lang = LANGUAGES.find((e) => e.code === currentLang)!;
|
||||
|
||||
return (
|
||||
<div className={s.wrap}>
|
||||
<Icon icon="language" className={s.icon} />
|
||||
{lang.name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LangSelector;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './LangSelect';
|
|
@ -0,0 +1,63 @@
|
|||
import React, { FC, MouseEvent } from 'react';
|
||||
import { Link as L, LinkProps as LProps } from 'react-router-dom';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { linkPathBuilder, RoutePath, LinkParams, LinkParamsKeys } from 'Paths';
|
||||
|
||||
interface LinkProps {
|
||||
to: RoutePath;
|
||||
props?: LinkParams;
|
||||
className?: string;
|
||||
type?: LProps['type'];
|
||||
stop?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const Link: FC<LinkProps> = ({
|
||||
to, children, className, props, type, stop, disabled, onClick, id,
|
||||
}) => {
|
||||
if (props) {
|
||||
Object.keys(props).forEach((key: unknown) => {
|
||||
if (!props[key as LinkParamsKeys]) {
|
||||
throw new Error(`Got wrong ${key} propKey: ${props[key as LinkParamsKeys]} in Link`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (stop) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
tabIndex={0}
|
||||
className={cn(className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<L
|
||||
id={id}
|
||||
className={className}
|
||||
type={type}
|
||||
to={linkPathBuilder(to, props)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
</L>
|
||||
);
|
||||
};
|
||||
|
||||
export default Link;
|
|
@ -0,0 +1,26 @@
|
|||
.mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1040;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.45);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity var(--transition);
|
||||
cursor: pointer;
|
||||
|
||||
&_visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (--l-viewport) {
|
||||
&_visible {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import React, { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
import s from './Mask.module.pcss';
|
||||
|
||||
interface MaskProps {
|
||||
open: boolean;
|
||||
handle: () => void;
|
||||
}
|
||||
|
||||
const Mask: FC<MaskProps> = ({ open, handle }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
s.mask,
|
||||
{ [s.mask_visible]: open },
|
||||
)}
|
||||
onClick={handle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mask;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Mask';
|
|
@ -1,2 +1,6 @@
|
|||
export { default as Icon } from './Icon';
|
||||
export { notifyError, notifySuccess } from './Notifications';
|
||||
export { default as Link } from './Link';
|
||||
export { default as LangSelect } from './LangSelect';
|
||||
export { default as Mask } from './Mask';
|
||||
export { CommonLayout, InnerLayout, CommonModalLayout, ConfirmModalLayout } from './layouts';
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { Layout } from 'antd';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
interface CommonLayoutProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CommonLayout: FC<CommonLayoutProps> = ({ children, className }) => {
|
||||
return (
|
||||
<Layout className={className}>
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonLayout;
|
|
@ -0,0 +1,87 @@
|
|||
import React, { FC, useContext, useEffect } from 'react';
|
||||
import { Modal, Button } from 'antd';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { Icon } from 'Common/ui';
|
||||
import Store from 'Store';
|
||||
|
||||
interface CommonModalLayoutProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
buttonText?: string;
|
||||
className?: string;
|
||||
width?: number;
|
||||
onClose: () => void;
|
||||
onSubmit?: () => void;
|
||||
noFooter?: boolean;
|
||||
disabled?: boolean;
|
||||
centered?: boolean;
|
||||
}
|
||||
|
||||
const CommonModalLayout: FC<CommonModalLayoutProps> = ({
|
||||
visible,
|
||||
children,
|
||||
title,
|
||||
buttonText,
|
||||
className,
|
||||
width,
|
||||
onClose,
|
||||
onSubmit,
|
||||
noFooter,
|
||||
disabled,
|
||||
centered,
|
||||
}) => {
|
||||
const store = useContext(Store);
|
||||
const { ui: { intl } } = store;
|
||||
|
||||
useEffect(() => {
|
||||
const onEnter = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && onSubmit) {
|
||||
onSubmit();
|
||||
}
|
||||
};
|
||||
if (onSubmit) {
|
||||
window.addEventListener('keyup', onEnter);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('keyup', onEnter);
|
||||
};
|
||||
}, [onSubmit]);
|
||||
const footer = noFooter ? null : [
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
key="submit"
|
||||
htmlType="submit"
|
||||
onClick={onSubmit}
|
||||
disabled={disabled}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>,
|
||||
<Button
|
||||
type="link"
|
||||
size="large"
|
||||
key="cancel"
|
||||
onClick={onClose}
|
||||
>
|
||||
{intl.getMessage('cancel')}
|
||||
</Button>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={title}
|
||||
wrapClassName={cn('modal', className)}
|
||||
onCancel={onClose}
|
||||
footer={footer}
|
||||
closeIcon={<Icon icon="close_big" />}
|
||||
width={width || 480}
|
||||
centered={centered}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonModalLayout;
|
|
@ -0,0 +1,34 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import CommonModalLayout from './CommonModalLayout';
|
||||
|
||||
interface DeleteModalLayoutProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
buttonText: string;
|
||||
onClose: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
const DeleteModalLayout: FC<DeleteModalLayoutProps> = ({
|
||||
visible,
|
||||
children,
|
||||
title,
|
||||
buttonText,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}) => {
|
||||
return (
|
||||
<CommonModalLayout
|
||||
visible={visible}
|
||||
title={title}
|
||||
buttonText={buttonText}
|
||||
onSubmit={onConfirm}
|
||||
onClose={onClose}
|
||||
>
|
||||
{children}
|
||||
</CommonModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteModalLayout;
|
|
@ -0,0 +1,41 @@
|
|||
import { Layout } from 'antd';
|
||||
import React, { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
import theme from 'Lib/theme';
|
||||
|
||||
interface InnerLayoutProps {
|
||||
title: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const InnerLayout: FC<InnerLayoutProps> = ({
|
||||
children, title, className, containerClassName,
|
||||
}) => {
|
||||
return (
|
||||
<Layout
|
||||
className={cn(
|
||||
theme.content.content,
|
||||
theme.content.content_inner,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
theme.content.container,
|
||||
containerClassName,
|
||||
)}
|
||||
>
|
||||
<div className={theme.content.header}>
|
||||
<div className={theme.content.title}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default InnerLayout;
|
|
@ -0,0 +1,4 @@
|
|||
export { default as CommonLayout } from './CommonLayout';
|
||||
export { default as InnerLayout } from './InnerLayout';
|
||||
export { default as ConfirmModalLayout } from './ConfirmModalLayout';
|
||||
export { default as CommonModalLayout } from './CommonModalLayout';
|
|
@ -0,0 +1,47 @@
|
|||
.modal {
|
||||
& .ant-modal-close-x {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--black);
|
||||
border-radius: 2px;
|
||||
background-color: var(--white);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--cloud);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--borders-white);
|
||||
}
|
||||
|
||||
& svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
@media (--s-viewport) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
& svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-modal-close {
|
||||
top: 11px;
|
||||
right: 8px;
|
||||
|
||||
@media (--s-viewport) {
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
font-weight: 500;
|
||||
overflow: auto;
|
||||
z-index: 1041;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (--l-viewport) {
|
||||
position: sticky;
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
& .ant-menu-item-group {
|
||||
@media (--m-viewport) {
|
||||
&:last-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
.tabs {
|
||||
border-radius: 2px;
|
||||
background-color: var(--white);
|
||||
|
||||
& .ant-tabs-tab {
|
||||
padding: 10px 16px;
|
||||
margin-right: 10px;
|
||||
color: var(--gray900);
|
||||
transition: color var(--transition), background var(--transition);
|
||||
|
||||
&.ant-tabs-tab-active {
|
||||
background-color: #E6F4EA;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-tabs-left > .ant-tabs-nav .ant-tabs-tab {
|
||||
@media (--l-viewport) {
|
||||
min-width: 230px;
|
||||
margin-bottom: 7px;
|
||||
padding: 10px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-tabs-left > .ant-tabs-content-holder > .ant-tabs-content > .ant-tabs-tabpane {
|
||||
@media (--l-viewport) {
|
||||
padding: 24px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& .ant-tabs-tabpane {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
& .ant-tabs-nav-list {
|
||||
padding: 0 16px;
|
||||
|
||||
@media (--l-viewport) {
|
||||
padding: 24px 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,10 @@
|
|||
@text-color: #000;
|
||||
@link-hover-color: #4d995f;
|
||||
@link-active-color: #4d995f;
|
||||
@text-selection-bg: #e7efff;
|
||||
@layout-body-background: #f3f3f3;
|
||||
@layout-header-background: #131313;
|
||||
@menu-dark-submenu-bg: #131313;
|
||||
|
||||
@font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
@font-size-base: 14px;
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
@import '~antd/dist/antd.less';
|
||||
@import './ant-overrides.less';
|
||||
|
||||
::selection {
|
||||
color: #000;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import './Radio.pcss';
|
||||
import './Sidebar.pcss';
|
||||
import './Tabs.pcss';
|
||||
import './Modal.pcss';
|
||||
|
||||
const insertStyles = true;
|
||||
export default insertStyles;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import qs from 'qs';
|
||||
import AccessListResponse, { IAccessListResponse } from 'Entities/AccessListResponse';
|
||||
import AccessSetRequest, { IAccessSetRequest } from 'Entities/AccessSetRequest';
|
||||
import Client, { IClient } from 'Entities/Client';
|
||||
import ClientDelete, { IClientDelete } from 'Entities/ClientDelete';
|
||||
import ClientUpdate, { IClientUpdate } from 'Entities/ClientUpdate';
|
||||
|
@ -8,6 +10,40 @@ import ClientsFindEntry, { IClientsFindEntry } from 'Entities/ClientsFindEntry';
|
|||
// This file was autogenerated. Please do not change.
|
||||
// All changes will be overwrited on commit.
|
||||
export default class ClientsApi {
|
||||
static async accessList(): Promise<IAccessListResponse | Error> {
|
||||
return await fetch(`/control/access/list`, {
|
||||
method: 'GET',
|
||||
}).then(async (res) => {
|
||||
if (res.status === 200) {
|
||||
return res.json();
|
||||
} else {
|
||||
return new Error(String(res.status));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async accessSet(accesssetrequest: IAccessSetRequest): Promise<number | string[] | Error> {
|
||||
const haveError: string[] = [];
|
||||
const accesssetrequestValid = new AccessSetRequest(accesssetrequest);
|
||||
haveError.push(...accesssetrequestValid.validate());
|
||||
if (haveError.length > 0) {
|
||||
return Promise.resolve(haveError);
|
||||
}
|
||||
return await fetch(`/control/access/set`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(accesssetrequestValid.serialize()),
|
||||
}).then(async (res) => {
|
||||
if (res.status === 200) {
|
||||
return res.status;
|
||||
} else {
|
||||
return new Error(String(res.status));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async clientsAdd(client: IClient): Promise<number | string[] | Error> {
|
||||
const haveError: string[] = [];
|
||||
const clientValid = new Client(client);
|
||||
|
|
|
@ -2,6 +2,7 @@ import DhcpConfig, { IDhcpConfig } from 'Entities/DhcpConfig';
|
|||
import DhcpSearchResult, { IDhcpSearchResult } from 'Entities/DhcpSearchResult';
|
||||
import DhcpStaticLease, { IDhcpStaticLease } from 'Entities/DhcpStaticLease';
|
||||
import DhcpStatus, { IDhcpStatus } from 'Entities/DhcpStatus';
|
||||
import NetInterfaces, { INetInterfaces } from 'Entities/NetInterfaces';
|
||||
|
||||
// This file was autogenerated. Please do not change.
|
||||
// All changes will be overwrited on commit.
|
||||
|
@ -40,6 +41,18 @@ export default class DhcpApi {
|
|||
})
|
||||
}
|
||||
|
||||
static async dhcpInterfaces(): Promise<INetInterfaces | Error> {
|
||||
return await fetch(`/control/dhcp/interfaces`, {
|
||||
method: 'GET',
|
||||
}).then(async (res) => {
|
||||
if (res.status === 200) {
|
||||
return res.json();
|
||||
} else {
|
||||
return new Error(String(res.status));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async dhcpRemoveStaticLease(dhcpstaticlease: IDhcpStaticLease): Promise<number | string[] | Error> {
|
||||
const haveError: string[] = [];
|
||||
const dhcpstaticleaseValid = new DhcpStaticLease(dhcpstaticlease);
|
||||
|
|
|
@ -3,9 +3,10 @@ import qs from 'qs';
|
|||
// This file was autogenerated. Please do not change.
|
||||
// All changes will be overwrited on commit.
|
||||
export default class MobileconfigApi {
|
||||
static async mobileConfigDoH(host?: string): Promise<number | Error> {
|
||||
static async mobileConfigDoH(host?: string, client_id?: string): Promise<number | Error> {
|
||||
const queryParams = {
|
||||
host: host,
|
||||
client_id: client_id,
|
||||
}
|
||||
return await fetch(`/control/apple/doh.mobileconfig?${qs.stringify(queryParams, { arrayFormat: 'comma' })}`, {
|
||||
method: 'GET',
|
||||
|
@ -18,9 +19,10 @@ export default class MobileconfigApi {
|
|||
})
|
||||
}
|
||||
|
||||
static async mobileConfigDoT(host?: string): Promise<number | Error> {
|
||||
static async mobileConfigDoT(host?: string, client_id?: string): Promise<number | Error> {
|
||||
const queryParams = {
|
||||
host: host,
|
||||
client_id: client_id,
|
||||
}
|
||||
return await fetch(`/control/apple/dot.mobileconfig?${qs.stringify(queryParams, { arrayFormat: 'comma' })}`, {
|
||||
method: 'GET',
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export const DEFAULT_NOTIFICATION_DURATION = 5;
|
||||
|
||||
export const DHCP_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP';
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
// This file was autogenerated. Please do not change.
|
||||
// All changes will be overwrited on commit.
|
||||
export interface IAccessList {
|
||||
allowed_clients?: string[];
|
||||
blocked_hosts?: string[];
|
||||
disallowed_clients?: string[];
|
||||
}
|
||||
|
||||
export default class AccessList {
|
||||
readonly _allowed_clients: string[] | undefined;
|
||||
|
||||
/** */
|
||||
get allowedClients(): string[] | undefined {
|
||||
return this._allowed_clients;
|
||||
}
|
||||
|
||||
readonly _blocked_hosts: string[] | undefined;
|
||||
|
||||
/** */
|
||||
get blockedHosts(): string[] | undefined {
|
||||
return this._blocked_hosts;
|
||||
}
|
||||
|
||||
readonly _disallowed_clients: string[] | undefined;
|
||||
|
||||
/** */
|
||||
get disallowedClients(): string[] | undefined {
|
||||
return this._disallowed_clients;
|
||||
}
|
||||
|
||||
constructor(props: IAccessList) {
|
||||
if (props.allowed_clients) {
|
||||
this._allowed_clients = props.allowed_clients;
|
||||
}
|
||||
if (props.blocked_hosts) {
|
||||
this._blocked_hosts = props.blocked_hosts;
|
||||
}
|
||||
if (props.disallowed_clients) {
|
||||
this._disallowed_clients = props.disallowed_clients;
|
||||
}
|
||||
}
|
||||
|
||||
serialize(): IAccessList {
|
||||
const data: IAccessList = {
|
||||
};
|
||||
if (typeof this._allowed_clients !== 'undefined') {
|
||||
data.allowed_clients = this._allowed_clients;
|
||||
}
|
||||
if (typeof this._blocked_hosts !== 'undefined') {
|
||||
data.blocked_hosts = this._blocked_hosts;
|
||||
}
|
||||
if (typeof this._disallowed_clients !== 'undefined') {
|
||||
data.disallowed_clients = this._disallowed_clients;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
validate(): string[] {
|
||||
const validate = {
|
||||
allowed_clients: !this._allowed_clients ? true : this._allowed_clients.reduce((result, p) => result && typeof p === 'string', true),
|
||||
disallowed_clients: !this._disallowed_clients ? true : this._disallowed_clients.reduce((result, p) => result && typeof p === 'string', true),
|
||||
blocked_hosts: !this._blocked_hosts ? true : this._blocked_hosts.reduce((result, p) => result && typeof p === 'string', true),
|
||||
};
|
||||
const isError: string[] = [];
|
||||
Object.keys(validate).forEach((key) => {
|
||||
if (!(validate as any)[key]) {
|
||||
isError.push(key);
|
||||
}
|
||||
});
|
||||
return isError;
|
||||
}
|
||||
|
||||
update(props: Partial<IAccessList>): AccessList {
|
||||
return new AccessList({ ...this.serialize(), ...props });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import AccessList, { IAccessList } from './AccessList';
|
||||
|
||||
// This file was autogenerated. Please do not change.
|
||||
// All changes will be overwrited on commit.
|
||||
export type IAccessListResponse = IAccessList;
|
||||
export default AccessList;
|
|
@ -0,0 +1,6 @@
|
|||
import AccessList, { IAccessList } from './AccessList';
|
||||
|
||||
// This file was autogenerated. Please do not change.
|
||||
// All changes will be overwrited on commit.
|
||||
export type IAccessSetRequest = IAccessList;
|
||||
export default AccessList;
|
|
@ -1,10 +1,10 @@
|
|||
import NetInterface, { INetInterface } from './NetInterface';
|
||||
import NetInterfaces, { INetInterfaces } from './NetInterfaces';
|
||||
|
||||
// This file was autogenerated. Please do not change.
|
||||
// All changes will be overwrited on commit.
|
||||
export interface IAddressesInfo {
|
||||
dns_port: number;
|
||||
interfaces: { [key: string]: INetInterface };
|
||||
interfaces: INetInterfaces;
|
||||
web_port: number;
|
||||
}
|
||||
|
||||
|
@ -23,10 +23,9 @@ export default class AddressesInfo {
|
|||
return typeof dnsPort === 'number';
|
||||
}
|
||||
|
||||
readonly _interfaces: { [key: string]: NetInterface };
|
||||
readonly _interfaces: NetInterfaces;
|
||||
|
||||
/** */
|
||||
get interfaces(): { [key: string]: NetInterface } {
|
||||
get interfaces(): NetInterfaces {
|
||||
return this._interfaces;
|
||||
}
|
||||
|
||||
|
@ -46,16 +45,14 @@ export default class AddressesInfo {
|
|||
|
||||
constructor(props: IAddressesInfo) {
|
||||
this._dns_port = props.dns_port;
|
||||
this._interfaces = Object.keys(props.interfaces).reduce((prev, key) => {
|
||||
return { ...prev, [key]: new NetInterface(props.interfaces[key])};
|
||||
},{})
|
||||
this._interfaces = new NetInterfaces(props.interfaces);
|
||||
this._web_port = props.web_port;
|
||||
}
|
||||
|
||||
serialize(): IAddressesInfo {
|
||||
const data: IAddressesInfo = {
|
||||
dns_port: this._dns_port,
|
||||
interfaces: Object.keys(this._interfaces).reduce<Record<string, any>>((prev, key) => ({ ...prev, [key]: this._interfaces[key].serialize() }), {}),
|
||||
interfaces: this._interfaces.serialize(),
|
||||
web_port: this._web_port,
|
||||
};
|
||||
return data;
|
||||
|
@ -65,6 +62,7 @@ export default class AddressesInfo {
|
|||
const validate = {
|
||||
dns_port: typeof this._dns_port === 'number',
|
||||
web_port: typeof this._web_port === 'number',
|
||||
interfaces: this._interfaces.validate().length === 0,
|
||||
};
|
||||
const isError: string[] = [];
|
||||
Object.keys(validate).forEach((key) => {
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface IClientFindSubEntry {
|
|||
upstreams?: string[];
|
||||
use_global_blocked_services?: boolean;
|
||||
use_global_settings?: boolean;
|
||||
whois_info?: IWhoisInfo[];
|
||||
whois_info?: IWhoisInfo;
|
||||
}
|
||||
|
||||
export default class ClientFindSubEntry {
|
||||
|
@ -98,9 +98,9 @@ export default class ClientFindSubEntry {
|
|||
return this._use_global_settings;
|
||||
}
|
||||
|
||||
readonly _whois_info: WhoisInfo[] | undefined;
|
||||
readonly _whois_info: WhoisInfo | undefined;
|
||||
|
||||
get whoisInfo(): WhoisInfo[] | undefined {
|
||||
get whoisInfo(): WhoisInfo | undefined {
|
||||
return this._whois_info;
|
||||
}
|
||||
|
||||
|
@ -142,7 +142,7 @@ export default class ClientFindSubEntry {
|
|||
this._use_global_settings = props.use_global_settings;
|
||||
}
|
||||
if (props.whois_info) {
|
||||
this._whois_info = props.whois_info.map((p) => new WhoisInfo(p));
|
||||
this._whois_info = new WhoisInfo(props.whois_info);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,7 +186,7 @@ export default class ClientFindSubEntry {
|
|||
data.use_global_settings = this._use_global_settings;
|
||||
}
|
||||
if (typeof this._whois_info !== 'undefined') {
|
||||
data.whois_info = this._whois_info.map((p) => p.serialize());
|
||||
data.whois_info = this._whois_info.serialize();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ export default class ClientFindSubEntry {
|
|||
use_global_blocked_services: !this._use_global_blocked_services ? true : typeof this._use_global_blocked_services === 'boolean',
|
||||
blocked_services: !this._blocked_services ? true : this._blocked_services.reduce((result, p) => result && typeof p === 'string', true),
|
||||
upstreams: !this._upstreams ? true : this._upstreams.reduce((result, p) => result && typeof p === 'string', true),
|
||||
whois_info: !this._whois_info ? true : this._whois_info.reduce((result, p) => result && p.validate().length === 0, true),
|
||||
whois_info: !this._whois_info ? true : this._whois_info.validate().length === 0,
|
||||
disallowed: !this._disallowed ? true : typeof this._disallowed === 'boolean',
|
||||
disallowed_rule: !this._disallowed_rule ? true : typeof this._disallowed_rule === 'string' && !this._disallowed_rule ? true : this._disallowed_rule,
|
||||
};
|
||||
|
|
|
@ -1,31 +1,33 @@
|
|||
import ClientFindSubEntry, { IClientFindSubEntry } from './ClientFindSubEntry';
|
||||
|
||||
// This file was autogenerated. Please do not change.
|
||||
// All changes will be overwrited on commit.
|
||||
export interface IClientsFindEntry {
|
||||
[key: string]: IClientFindSubEntry;
|
||||
}
|
||||
|
||||
export default class ClientsFindEntry {
|
||||
readonly data: Record<string, ClientFindSubEntry>;
|
||||
|
||||
constructor(props: IClientsFindEntry) {
|
||||
this.data = Object.entries(props).reduce<Record<string, ClientFindSubEntry>>((prev, [key, value]) => {
|
||||
prev[key] = new ClientFindSubEntry(value!);
|
||||
return prev;
|
||||
}, {})
|
||||
}
|
||||
|
||||
serialize(): IClientsFindEntry {
|
||||
const data: IClientsFindEntry = {
|
||||
};
|
||||
return data;
|
||||
return Object.entries(this.data).reduce<Record<string, IClientFindSubEntry>>((prev, [key, value]) => {
|
||||
prev[key] = value.serialize();
|
||||
return prev;
|
||||
}, {})
|
||||
}
|
||||
|
||||
validate(): string[] {
|
||||
const validate = {
|
||||
};
|
||||
const isError: string[] = [];
|
||||
Object.keys(validate).forEach((key) => {
|
||||
if (!(validate as any)[key]) {
|
||||
isError.push(key);
|
||||
}
|
||||
});
|
||||
return isError;
|
||||
return []
|
||||
}
|
||||
|
||||
update(props: Partial<IClientsFindEntry>): ClientsFindEntry {
|
||||
update(props: IClientsFindEntry): ClientsFindEntry {
|
||||
return new ClientsFindEntry({ ...this.serialize(), ...props });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ export default class DhcpSearchResultOtherServer {
|
|||
readonly _found: string | undefined;
|
||||
|
||||
/**
|
||||
* Description: yes|no|error
|
||||
* Description: The result of searching the other DHCP server.
|
||||
*
|
||||
* Example: no
|
||||
*/
|
||||
get found(): string | undefined {
|
||||
|
|
|
@ -16,7 +16,8 @@ export default class DhcpSearchResultStaticIP {
|
|||
readonly _static: string | undefined;
|
||||
|
||||
/**
|
||||
* Description: yes|no|error
|
||||
* Description: The result of determining static IP address.
|
||||
*
|
||||
* Example: yes
|
||||
*/
|
||||
get static(): string | undefined {
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
export interface IFilter {
|
||||
enabled: boolean;
|
||||
id: number;
|
||||
lastUpdated: string;
|
||||
last_updated: string;
|
||||
name: string;
|
||||
rulesCount: number;
|
||||
rules_count: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
|
@ -34,14 +34,14 @@ export default class Filter {
|
|||
return typeof id === 'number';
|
||||
}
|
||||
|
||||
readonly _lastUpdated: string;
|
||||
readonly _last_updated: string;
|
||||
|
||||
/**
|
||||
* Description: undefined
|
||||
* Example: 2018-10-30T12:18:57+03:00
|
||||
*/
|
||||
get lastUpdated(): string {
|
||||
return this._lastUpdated;
|
||||
return this._last_updated;
|
||||
}
|
||||
|
||||
static lastUpdatedValidate(lastUpdated: string): boolean {
|
||||
|
@ -62,14 +62,14 @@ export default class Filter {
|
|||
return typeof name === 'string' && !!name.trim();
|
||||
}
|
||||
|
||||
readonly _rulesCount: number;
|
||||
readonly _rules_count: number;
|
||||
|
||||
/**
|
||||
* Description: undefined
|
||||
* Example: 5912
|
||||
*/
|
||||
get rulesCount(): number {
|
||||
return this._rulesCount;
|
||||
return this._rules_count;
|
||||
}
|
||||
|
||||
static rulesCountValidate(rulesCount: number): boolean {
|
||||
|
@ -94,9 +94,9 @@ export default class Filter {
|
|||
constructor(props: IFilter) {
|
||||
this._enabled = props.enabled;
|
||||
this._id = props.id;
|
||||
this._lastUpdated = props.lastUpdated.trim();
|
||||
this._last_updated = props.last_updated.trim();
|
||||
this._name = props.name.trim();
|
||||
this._rulesCount = props.rulesCount;
|
||||
this._rules_count = props.rules_count;
|
||||
this._url = props.url.trim();
|
||||
}
|
||||
|
||||
|
@ -104,9 +104,9 @@ export default class Filter {
|
|||
const data: IFilter = {
|
||||
enabled: this._enabled,
|
||||
id: this._id,
|
||||
lastUpdated: this._lastUpdated,
|
||||
last_updated: this._last_updated,
|
||||
name: this._name,
|
||||
rulesCount: this._rulesCount,
|
||||
rules_count: this._rules_count,
|
||||
url: this._url,
|
||||
};
|
||||
return data;
|
||||
|
@ -116,9 +116,9 @@ export default class Filter {
|
|||
const validate = {
|
||||
enabled: typeof this._enabled === 'boolean',
|
||||
id: typeof this._id === 'number',
|
||||
lastUpdated: typeof this._lastUpdated === 'string' && !this._lastUpdated ? true : this._lastUpdated,
|
||||
last_updated: typeof this._last_updated === 'string' && !this._last_updated ? true : this._last_updated,
|
||||
name: typeof this._name === 'string' && !this._name ? true : this._name,
|
||||
rulesCount: typeof this._rulesCount === 'number',
|
||||
rules_count: typeof this._rules_count === 'number',
|
||||
url: typeof this._url === 'string' && !this._url ? true : this._url,
|
||||
};
|
||||
const isError: string[] = [];
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
// This file was autogenerated. Please do not change.
|
||||
// All changes will be overwrited on commit.
|
||||
export interface ILogin {
|
||||
name?: string;
|
||||
password?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export default class Login {
|
||||
readonly _name: string | undefined;
|
||||
|
||||
/** */
|
||||
get name(): string | undefined {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
readonly _password: string | undefined;
|
||||
|
||||
/** */
|
||||
|
@ -13,37 +20,30 @@ export default class Login {
|
|||
return this._password;
|
||||
}
|
||||
|
||||
readonly _username: string | undefined;
|
||||
|
||||
/** */
|
||||
get username(): string | undefined {
|
||||
return this._username;
|
||||
}
|
||||
|
||||
constructor(props: ILogin) {
|
||||
if (typeof props.name === 'string') {
|
||||
this._name = props.name.trim();
|
||||
}
|
||||
if (typeof props.password === 'string') {
|
||||
this._password = props.password.trim();
|
||||
}
|
||||
if (typeof props.username === 'string') {
|
||||
this._username = props.username.trim();
|
||||
}
|
||||
}
|
||||
|
||||
serialize(): ILogin {
|
||||
const data: ILogin = {
|
||||
};
|
||||
if (typeof this._name !== 'undefined') {
|
||||
data.name = this._name;
|
||||
}
|
||||
if (typeof this._password !== 'undefined') {
|
||||
data.password = this._password;
|
||||
}
|
||||
if (typeof this._username !== 'undefined') {
|
||||
data.username = this._username;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
validate(): string[] {
|
||||
const validate = {
|
||||
username: !this._username ? true : typeof this._username === 'string' && !this._username ? true : this._username,
|
||||
name: !this._name ? true : typeof this._name === 'string' && !this._name ? true : this._name,
|
||||
password: !this._password ? true : typeof this._password === 'string' && !this._password ? true : this._password,
|
||||
};
|
||||
const isError: string[] = [];
|
||||
|
|
|
@ -12,7 +12,8 @@ export default class NetInterface {
|
|||
readonly _flags: string;
|
||||
|
||||
/**
|
||||
* Description: undefined
|
||||
* Description: Flags could be any combination of the following values, divided by the "|" character: "up", "broadcast", "loopback", "pointtopoint" and "multicast".
|
||||
*
|
||||
* Example: up|broadcast|multicast
|
||||
*/
|
||||
get flags(): string {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue