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: 113743a6 9cd9054c
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: fd91a0a3 3e0238aa
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: a195f1f4 154c9c1c
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:
Vladislav Abdulmyanov 2021-02-03 16:14:20 +03:00
parent 9cd9054cdb
commit 0c127039cf
136 changed files with 5384 additions and 2938 deletions

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as BlockCard } from './BlockCard';

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as BlockedQueries } from './BlockedQueries';

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as ServerStatistics } from './ServerStatistics';

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as TopClients } from './TopClients';

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as TopDomains } from './TopDomains';

View File

@ -0,0 +1,5 @@
export { BlockCard } from './BlockCard';
export { TopClients } from './TopClients';
export { TopDomains } from './TopDomains';
export { BlockedQueries } from './BlockedQueries';
export { ServerStatistics } from './ServerStatistics';

View File

@ -0,0 +1 @@
export { default } from './Dashboard';

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as ErrorBoundary } from './ErrorBoundary';

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './Header';

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export { default as Login } from './Login';
export { default as ForgotPassword } from './ForgotPassword';

View File

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

View File

@ -0,0 +1,3 @@
.app {
min-height: 100vh;
}

View File

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

View File

@ -0,0 +1 @@
export { default } from './Routes';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default as GeneralSettings } from './GeneralSettings';

View File

@ -0,0 +1 @@
export { GeneralSettings } from './GeneralSettings';

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './SetupGuide';

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './Sidebar';

View File

@ -0,0 +1 @@
export { default } from './App';

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './Button';

View File

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

View File

@ -1,3 +1,4 @@
export { default as Radio } from './Radio';
export { Input } from './Input';
export { Switch } from './Switch';
export { default as Button } from './Button';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
.wrap {
display: inline-flex;
align-items: center;
}
.icon {
font-size: 22px;
margin-right: 10px;
color: var(--gray700);
}

View File

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

View File

@ -0,0 +1 @@
export { default } from './LangSelect';

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './Mask';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1,6 @@
@import '~antd/dist/antd.less';
@import './ant-overrides.less';
::selection {
color: #000;
}

View File

@ -1,4 +1,7 @@
import './Radio.pcss';
import './Sidebar.pcss';
import './Tabs.pcss';
import './Modal.pcss';
const insertStyles = true;
export default insertStyles;

View File

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

View File

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

View File

@ -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',

View File

@ -1 +1,3 @@
export const DEFAULT_NOTIFICATION_DURATION = 5;
export const DHCP_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP';

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -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[] = [];

View File

@ -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[] = [];

View File

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