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: 113743a69cd9054cAuthor: 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: fd91a0a33e0238aaAuthor: 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: a195f1f4154c9c1cAuthor: 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:
@@ -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;
|
||||
20
client2/src/components/App/App.tsx
Normal file
20
client2/src/components/App/App.tsx
Normal 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;
|
||||
136
client2/src/components/App/Dashboard/Dashboard.tsx
Normal file
136
client2/src/components/App/Dashboard/Dashboard.tsx
Normal 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;
|
||||
@@ -0,0 +1,20 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: 24px;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--gray700);
|
||||
}
|
||||
|
||||
.overal {
|
||||
font-size: 30px;
|
||||
line-height: 38px;
|
||||
margin-bottom: 18px;
|
||||
color: var(--gray900);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { FC } from 'react';
|
||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
||||
|
||||
import s from './BlockCard.module.pcss';
|
||||
|
||||
interface BlockCardProps {
|
||||
overal: number | string;
|
||||
data?: number[];
|
||||
text?: string;
|
||||
color?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const BlockCard: FC<BlockCardProps> = ({ overal, data, color, title, text }) => {
|
||||
return (
|
||||
<div className={s.container}>
|
||||
<div className={s.title}>{title}</div>
|
||||
<div className={s.overal}>{overal}</div>
|
||||
{data && (
|
||||
<ResponsiveContainer width="100%" height={25}>
|
||||
<AreaChart data={data.map((n) => ({ name: 'data', value: n }))}>
|
||||
<Area dataKey="value" stroke={color} fill={color} dot={false} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
{text && (
|
||||
<div>
|
||||
{text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockCard;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as BlockCard } from './BlockCard';
|
||||
@@ -0,0 +1,16 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: 24px;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--gray700);
|
||||
}
|
||||
.pie {
|
||||
padding: 34px 0px;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import theme from 'Lib/theme';
|
||||
import React, { FC, useContext, useState } from 'react';
|
||||
import { PieChart, Pie, ResponsiveContainer, Sector, Cell } from 'recharts';
|
||||
|
||||
import Store from 'Store';
|
||||
|
||||
import s from './BlockedQueries.module.pcss';
|
||||
|
||||
interface BlockCardProps {
|
||||
ads: number;
|
||||
trackers: number;
|
||||
other: number;
|
||||
}
|
||||
|
||||
const renderActiveShape = (props: any): any => {
|
||||
const {
|
||||
cx, cy, innerRadius, outerRadius, startAngle, endAngle,
|
||||
fill, payload, percent,
|
||||
} = props;
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy - 11} dy={8} textAnchor="middle" fill={fill}>{payload.name}</text>
|
||||
<text x={cx} y={cy + 18} dy={8} fontSize={24} textAnchor="middle" >{Math.round(percent * 100)}%</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius + 5}
|
||||
outerRadius={outerRadius + 5}
|
||||
startAngle={startAngle + 1}
|
||||
endAngle={endAngle - 1}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockedQueries: FC<BlockCardProps> = ({ ads, trackers, other }) => {
|
||||
const store = useContext(Store);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const { ui: { intl } } = store;
|
||||
const data = [
|
||||
{ name: intl.getMessage('other'), value: other, color: theme.chartColors.gray700 },
|
||||
{ name: intl.getMessage('ads'), value: ads, color: theme.chartColors.red },
|
||||
{ name: intl.getMessage('trackers'), value: trackers, color: theme.chartColors.orange },
|
||||
];
|
||||
const onChart: any = (_: any, index: number) => {
|
||||
setActiveIndex(index);
|
||||
};
|
||||
return (
|
||||
<div className={s.container}>
|
||||
<div className={s.title}>{intl.getMessage('dashboard_blocked_queries')}</div>
|
||||
<div className={s.pie}>
|
||||
<ResponsiveContainer width="100%" height={190}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
activeIndex={activeIndex}
|
||||
data={data}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
activeShape={renderActiveShape}
|
||||
onClick={onChart}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockedQueries;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as BlockedQueries } from './BlockedQueries';
|
||||
@@ -0,0 +1,46 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
background-color: var(--white);
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
color: var(--gray900);
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cardBorder {
|
||||
border-right: 1px solid var(--gray300);
|
||||
|
||||
&:last-of-type {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cardDesc {
|
||||
color: var(--gray700);
|
||||
}
|
||||
|
||||
.cardValue {
|
||||
color: var(--gray900);
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { Row, Col } from 'antd';
|
||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
||||
|
||||
import Store from 'Store';
|
||||
import theme from 'Lib/theme';
|
||||
|
||||
import s from './ServerStatistics.module.pcss';
|
||||
|
||||
const ServerStatistics: FC = () => {
|
||||
const store = useContext(Store);
|
||||
const { ui: { intl } } = store;
|
||||
|
||||
const data = [0, 10, 2, 14, 12, 24, 5, 8, 10, 0, 3, 5, 7, 8, 3];
|
||||
return (
|
||||
<div className={s.container}>
|
||||
<div className={s.title}>{intl.getMessage('dashboard_server_statistics')}</div>
|
||||
<Row>
|
||||
<Col span={24} md={6} className={s.cardBorder}>
|
||||
<div className={s.card}>
|
||||
<div className={s.cardTitle}>
|
||||
Average server load
|
||||
</div>
|
||||
<div className={s.cardDesc}>
|
||||
<div>
|
||||
Processes: 213
|
||||
</div>
|
||||
<div>
|
||||
Cores: 2
|
||||
</div>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={25} className={s.chart}>
|
||||
<AreaChart data={data.map((n) => ({ name: 'data', value: n }))}>
|
||||
<Area dataKey="value" stroke={theme.chartColors.green} fill={theme.chartColors.green} dot={false} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24} md={6} className={s.cardBorder}>
|
||||
<div className={s.card}>
|
||||
<div className={s.cardTitle}>
|
||||
Memory usage
|
||||
</div>
|
||||
<div className={s.cardValue}>
|
||||
236 Mb
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={25} className={s.chart}>
|
||||
<AreaChart data={data.map((n) => ({ name: 'data', value: n }))}>
|
||||
<Area dataKey="value" stroke={theme.chartColors.orange} fill={theme.chartColors.orange} dot={false} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24} md={6} className={s.cardBorder}>
|
||||
<div className={s.card}>
|
||||
<div className={s.cardTitle}>
|
||||
DNS cashe size
|
||||
</div>
|
||||
<div className={s.cardValue}>
|
||||
2 363 records
|
||||
</div>
|
||||
<div className={s.cardDesc}>
|
||||
<div>
|
||||
32 Mb
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24} md={6} className={s.cardBorder}>
|
||||
<div className={s.card}>
|
||||
<div className={s.cardTitle}>
|
||||
Upstream servers data
|
||||
</div>
|
||||
<div className={s.cardDesc}>
|
||||
<div>
|
||||
Processes: 213
|
||||
</div>
|
||||
<div>
|
||||
Cores: 2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerStatistics;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ServerStatistics } from './ServerStatistics';
|
||||
@@ -0,0 +1,43 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
margin-bottom: 4px;
|
||||
padding: 24px;
|
||||
color: var(--gray900);
|
||||
}
|
||||
|
||||
.table {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tableTitle {
|
||||
color: var(--gray700);
|
||||
background-color: #fafafa;
|
||||
padding: 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.tableGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 4fr 1fr 1fr 1.5fr 1fr .5fr;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
> div {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
.ids {
|
||||
color: var(--gray700)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import Store from 'Store';
|
||||
|
||||
import s from './TopClients.module.pcss';
|
||||
|
||||
const TopClients: FC = observer(() => {
|
||||
const store = useContext(Store);
|
||||
const { ui: { intl }, dashboard } = store;
|
||||
const { clientsInfo, stats } = dashboard;
|
||||
const topClients = new Map();
|
||||
stats?.topClients?.forEach((client) => {
|
||||
const [id, requests] = Object.entries(client.numberData);
|
||||
topClients.set(id, requests);
|
||||
});
|
||||
const clients = Array.from(clientsInfo.entries());
|
||||
|
||||
return (
|
||||
<div className={s.container}>
|
||||
<div className={s.title}>{intl.getMessage('Top Clients')}</div>
|
||||
<div className={s.table}>
|
||||
<div className={cn(s.tableTitle, s.tableGrid)}>
|
||||
<div>{intl.getMessage('client_table_header')}</div>
|
||||
<div>{intl.getMessage('requests')}</div>
|
||||
<div>{intl.getMessage('show_blocked_responses')}</div>
|
||||
<div>%</div>
|
||||
<div/>
|
||||
<div/>
|
||||
</div>
|
||||
{clients.map(([id, c]) => {
|
||||
const request = topClients.get(id);
|
||||
return (
|
||||
<div className={s.tableGrid} key={id}>
|
||||
<div>
|
||||
{c.name}
|
||||
<div className={s.ids}>
|
||||
{c.ids?.map((cid) => (
|
||||
<div key={cid}>{cid}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{request}
|
||||
</div>
|
||||
<div>
|
||||
API
|
||||
{/* TODO: api */}
|
||||
</div>
|
||||
<div>
|
||||
API / {request}
|
||||
</div>
|
||||
<div>
|
||||
<Button>
|
||||
{intl.getMessage('Block')}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default TopClients;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as TopClients } from './TopClients';
|
||||
@@ -0,0 +1,62 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
margin-bottom: 16px;
|
||||
color: var(--gray900);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
|
||||
}
|
||||
|
||||
.overal {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
margin-bottom: 24px;
|
||||
color: var(--gray900);
|
||||
}
|
||||
|
||||
.table {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
max-height: 280px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
/* TODO: color */
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: inherit;
|
||||
background-color: #fafafa;
|
||||
font-weight: 500;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr 1.5fr;
|
||||
grid-column-gap: 10px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
}
|
||||
|
||||
.domain {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: flex;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { Progress } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
|
||||
|
||||
import TopArrayEntry from 'Entities/TopArrayEntry';
|
||||
import theme from 'Lib/theme';
|
||||
import Store from 'Store';
|
||||
|
||||
import s from './TopDomains.module.pcss';
|
||||
|
||||
interface TopDomainsProps {
|
||||
title: string;
|
||||
overal: number;
|
||||
chartData: number[];
|
||||
tableData: TopArrayEntry[];
|
||||
color: string;
|
||||
useValueColor?: boolean;
|
||||
}
|
||||
|
||||
const TopDomains: FC<TopDomainsProps> = (
|
||||
{ title, overal, chartData, tableData, color, useValueColor },
|
||||
) => {
|
||||
const store = useContext(Store);
|
||||
const { ui: { intl } } = store;
|
||||
const data = tableData.map((e) => {
|
||||
const [domain, value] = Object.entries(e.numberData)[0];
|
||||
return { domain, value };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={s.container}>
|
||||
<div className={s.title}>{title}</div>
|
||||
<div className={s.content}>
|
||||
<div className={s.overal}>
|
||||
{overal.toLocaleString('en')}
|
||||
<ResponsiveContainer width="100%" height={45}>
|
||||
<AreaChart data={chartData.map((n) => ({ name: 'data', value: n }))}>
|
||||
<Area dataKey="value" stroke={color} fill={color} dot={false} strokeWidth={2} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className={s.table}>
|
||||
<div className={cn(s.tableHeader, s.tableRow)}>
|
||||
<div>
|
||||
{intl.getMessage('domain')}
|
||||
</div>
|
||||
<div>
|
||||
{intl.getMessage('all_queries')}
|
||||
</div>
|
||||
<div>
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
{data.map(({ domain, value }) => (
|
||||
<div className={s.tableRow} key={domain}>
|
||||
<div className={s.domain}>{domain}</div>
|
||||
<div style={{ color: useValueColor ? color : 'initial' }}>{value}</div>
|
||||
<Progress
|
||||
percent={Math.round((value / overal) * 100)}
|
||||
strokeLinecap="square"
|
||||
strokeColor={theme.chartColors.gray700}
|
||||
trailColor={theme.chartColors.gray300}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopDomains;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as TopDomains } from './TopDomains';
|
||||
5
client2/src/components/App/Dashboard/components/index.ts
Normal file
5
client2/src/components/App/Dashboard/components/index.ts
Normal 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';
|
||||
1
client2/src/components/App/Dashboard/index.ts
Normal file
1
client2/src/components/App/Dashboard/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Dashboard';
|
||||
31
client2/src/components/App/Errors/ErrorBoundary.tsx
Normal file
31
client2/src/components/App/Errors/ErrorBoundary.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
79
client2/src/components/App/Errors/Errors.module.pcss
Normal file
79
client2/src/components/App/Errors/Errors.module.pcss
Normal 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;
|
||||
}
|
||||
}
|
||||
1
client2/src/components/App/Errors/index.ts
Normal file
1
client2/src/components/App/Errors/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ErrorBoundary } from './ErrorBoundary';
|
||||
81
client2/src/components/App/Header/Header.module.pcss
Normal file
81
client2/src/components/App/Header/Header.module.pcss
Normal 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;
|
||||
}
|
||||
}
|
||||
60
client2/src/components/App/Header/Header.tsx
Normal file
60
client2/src/components/App/Header/Header.tsx
Normal 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;
|
||||
1
client2/src/components/App/Header/index.ts
Normal file
1
client2/src/components/App/Header/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Header';
|
||||
65
client2/src/components/App/Login/ForgotPassword.tsx
Normal file
65
client2/src/components/App/Login/ForgotPassword.tsx
Normal 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;
|
||||
34
client2/src/components/App/Login/Login.module.pcss
Normal file
34
client2/src/components/App/Login/Login.module.pcss
Normal 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;
|
||||
}
|
||||
102
client2/src/components/App/Login/Login.tsx
Normal file
102
client2/src/components/App/Login/Login.tsx
Normal 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;
|
||||
2
client2/src/components/App/Login/index.ts
Normal file
2
client2/src/components/App/Login/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Login } from './Login';
|
||||
export { default as ForgotPassword } from './ForgotPassword';
|
||||
63
client2/src/components/App/Routes/Paths.ts
Normal file
63
client2/src/components/App/Routes/Paths.ts
Normal 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;
|
||||
};
|
||||
3
client2/src/components/App/Routes/Routes.module.pcss
Normal file
3
client2/src/components/App/Routes/Routes.module.pcss
Normal file
@@ -0,0 +1,3 @@
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
76
client2/src/components/App/Routes/Routes.tsx
Normal file
76
client2/src/components/App/Routes/Routes.tsx
Normal 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;
|
||||
1
client2/src/components/App/Routes/index.ts
Normal file
1
client2/src/components/App/Routes/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Routes';
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { FC, useContext, useEffect } from 'react';
|
||||
import { Tabs, Grid } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { InnerLayout } from 'Common/ui';
|
||||
import Store from 'Store';
|
||||
|
||||
import { General, QueryLog, Statistics, TAB_KEY } from './components';
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const GeneralSettings: FC = observer(() => {
|
||||
const store = useContext(Store);
|
||||
const { ui: { intl }, generalSettings } = store;
|
||||
const { inited } = generalSettings;
|
||||
const screens = useBreakpoint();
|
||||
|
||||
useEffect(() => {
|
||||
if (!inited) {
|
||||
generalSettings.init();
|
||||
}
|
||||
}, [inited]);
|
||||
|
||||
if (!inited) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabsPosition = screens.lg ? 'left' : 'top';
|
||||
|
||||
return (
|
||||
<InnerLayout title={intl.getMessage('general_settings')}>
|
||||
<Tabs
|
||||
defaultActiveKey={TAB_KEY.GENERAL}
|
||||
tabPosition={tabsPosition}
|
||||
className="tabs"
|
||||
>
|
||||
<TabPane tab={intl.getMessage('filter_category_general')} key={TAB_KEY.GENERAL}>
|
||||
<General/>
|
||||
</TabPane>
|
||||
<TabPane tab={intl.getMessage('query_log_configuration')} key={TAB_KEY.QUERY_LOG}>
|
||||
<QueryLog/>
|
||||
</TabPane>
|
||||
<TabPane tab={intl.getMessage('statistics_configuration')} key={TAB_KEY.STATISTICS}>
|
||||
<Statistics/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</InnerLayout>
|
||||
);
|
||||
});
|
||||
|
||||
export default GeneralSettings;
|
||||
@@ -0,0 +1,45 @@
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--gray900);
|
||||
margin-bottom: 48px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: block;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: -12px;
|
||||
}
|
||||
}
|
||||
.save {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.nameTitle {
|
||||
color: var(--black);
|
||||
}
|
||||
.nameDesc {
|
||||
color: var(--gray700);
|
||||
margin-right: 40px;
|
||||
|
||||
@media (--m-viewport) {
|
||||
margin-right: 200px;
|
||||
}
|
||||
}
|
||||
.select {
|
||||
margin-bottom: 24px;
|
||||
margin-top: -12px;
|
||||
width: 200px;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { Button, Switch, Select } from 'antd';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Link } from 'Common/ui';
|
||||
import Store from 'Store';
|
||||
import { RoutePath } from 'Paths';
|
||||
|
||||
import { s } from '.';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const General: FC = observer(() => {
|
||||
const store = useContext(Store);
|
||||
const { ui: { intl }, generalSettings } = store;
|
||||
const {
|
||||
safebrowsing,
|
||||
filteringConfig,
|
||||
parental,
|
||||
safesearch,
|
||||
} = generalSettings;
|
||||
|
||||
const initialValues = {
|
||||
...filteringConfig!.serialize(),
|
||||
safebrowsing,
|
||||
parental,
|
||||
safesearch,
|
||||
};
|
||||
|
||||
type InitialValues = typeof initialValues;
|
||||
|
||||
const onSubmit = async (values: InitialValues, helpers: FormikHelpers<InitialValues>) => {
|
||||
// await generalSettings.updateQueryLogConfig(values);
|
||||
if (initialValues.parental !== values.parental) {
|
||||
generalSettings[values.parental ? 'parentalEnable' : 'parentalDisable']();
|
||||
}
|
||||
if (initialValues.safesearch !== values.safesearch) {
|
||||
generalSettings[values.safesearch ? 'safebrowsingEnable' : 'safebrowsingDisable']();
|
||||
}
|
||||
if (initialValues.safebrowsing !== values.safebrowsing) {
|
||||
generalSettings[values.safebrowsing ? 'safebrowsingEnable' : 'safebrowsingDisable']();
|
||||
}
|
||||
if (initialValues.enabled !== values.enabled
|
||||
|| initialValues.interval !== values.interval) {
|
||||
generalSettings.updateFilteringConfig({
|
||||
interval: values.interval,
|
||||
enabled: values.enabled,
|
||||
});
|
||||
}
|
||||
helpers.setSubmitting(false);
|
||||
};
|
||||
|
||||
const filtersLink = (e: string) => {
|
||||
// TODO: fix link
|
||||
return <Link to={RoutePath.Dashboard}>{e}</Link>;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={s.title}>
|
||||
{intl.getMessage('filter_category_general')}
|
||||
</div>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({
|
||||
handleSubmit,
|
||||
values,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
dirty,
|
||||
}) => (
|
||||
<form onSubmit={handleSubmit} noValidate className={s.form}>
|
||||
<div className={s.item}>
|
||||
<div>
|
||||
<div className={s.nameTitle}>
|
||||
{intl.getMessage('block_domain_use_filters_and_hosts')}
|
||||
</div>
|
||||
<div className={s.nameDesc}>
|
||||
{intl.getMessage('filters_block_toggle_hint', { a: filtersLink })}
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={values.enabled} onChange={(e) => setFieldValue('enabled', e)}/>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<div>
|
||||
<div className={s.nameTitle}>
|
||||
{intl.getMessage('filters_interval')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={values.interval}
|
||||
onChange={(e) => setFieldValue('interval', e)}
|
||||
className={s.select}
|
||||
>
|
||||
<Option value={0}>
|
||||
{intl.getMessage('disabled')}
|
||||
</Option>
|
||||
<Option value={1}>
|
||||
{intl.getPlural('interval_hours', 1, { count: 1 })}
|
||||
</Option>
|
||||
<Option value={12}>
|
||||
{intl.getPlural('interval_hours', 12, { count: 12 })}
|
||||
</Option>
|
||||
<Option value={24}>
|
||||
{intl.getPlural('interval_hours', 24, { count: 24 })}
|
||||
</Option>
|
||||
<Option value={72}>
|
||||
{intl.getPlural('interval_days', 3, { count: 3 })}
|
||||
</Option>
|
||||
<Option value={168}>
|
||||
{intl.getPlural('interval_days', 7, { count: 7 })}
|
||||
</Option>
|
||||
</Select>
|
||||
<div className={s.item}>
|
||||
<div>
|
||||
<div className={s.nameTitle}>
|
||||
{intl.getMessage('use_adguard_browsing_sec')}
|
||||
</div>
|
||||
<div className={s.nameDesc}>
|
||||
{intl.getMessage('use_adguard_browsing_sec_hint')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={values.safebrowsing} onChange={(e) => setFieldValue('safebrowsing', e)}/>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<div>
|
||||
<div className={s.nameTitle}>
|
||||
{intl.getMessage('use_adguard_parental')}
|
||||
</div>
|
||||
<div className={s.nameDesc}>
|
||||
{intl.getMessage('use_adguard_parental_hint')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={values.parental} onChange={(e) => setFieldValue('parental', e)}/>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<div>
|
||||
<div className={s.nameTitle}>
|
||||
{intl.getMessage('enforce_safe_search')}
|
||||
</div>
|
||||
<div className={s.nameDesc}>
|
||||
{intl.getMessage('enforce_save_search_hint')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={values.safesearch} onChange={(e) => setFieldValue('safesearch', e)}/>
|
||||
</div>
|
||||
{dirty && (
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
className={s.save}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{intl.getMessage('save_btn')}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default General;
|
||||
@@ -0,0 +1,124 @@
|
||||
import React, { FC, useContext, useState } from 'react';
|
||||
import { Radio, Button, Switch } from 'antd';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { notifySuccess, ConfirmModalLayout } from 'Common/ui';
|
||||
import { IQueryLogConfig } from 'Entities/QueryLogConfig';
|
||||
import Store from 'Store';
|
||||
|
||||
import { s } from '.';
|
||||
|
||||
const { Group } = Radio;
|
||||
|
||||
const QueryLog: FC = observer(() => {
|
||||
const store = useContext(Store);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const { ui: { intl }, generalSettings } = store;
|
||||
const {
|
||||
queryLogConfig,
|
||||
} = generalSettings;
|
||||
|
||||
const onSubmit = async (values: IQueryLogConfig, helpers: FormikHelpers<IQueryLogConfig>) => {
|
||||
await generalSettings.updateQueryLogConfig(values);
|
||||
helpers.setSubmitting(false);
|
||||
};
|
||||
|
||||
const onReset = async () => {
|
||||
const result = await generalSettings.querylogClear();
|
||||
if (result) {
|
||||
notifySuccess(intl.getMessage('query_log_cleared'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={s.title}>
|
||||
{intl.getMessage('query_log_configuration')}
|
||||
<Button onClick={() => setShowConfirm(true)}>
|
||||
{intl.getMessage('query_log_clear')}
|
||||
</Button>
|
||||
</div>
|
||||
<ConfirmModalLayout
|
||||
visible={showConfirm}
|
||||
onConfirm={onReset}
|
||||
onClose={() => setShowConfirm(false)}
|
||||
title={intl.getMessage('query_log_clear')}
|
||||
buttonText={intl.getMessage('query_log_clear')}
|
||||
>
|
||||
{intl.getMessage('query_log_confirm_clear')}
|
||||
</ConfirmModalLayout>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={queryLogConfig!.serialize()}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({
|
||||
handleSubmit,
|
||||
values,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
dirty,
|
||||
}) => (
|
||||
<form onSubmit={handleSubmit} noValidate className={s.form}>
|
||||
<div className={s.item}>
|
||||
<div>
|
||||
<div className={s.nameTitle}>
|
||||
{intl.getMessage('query_log_enable')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={values.enabled} onChange={(e) => setFieldValue('enabled', e)}/>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<div>
|
||||
<div className={s.nameTitle}>
|
||||
{intl.getMessage('anonymize_client_ip')}
|
||||
</div>
|
||||
<div className={s.nameDesc}>
|
||||
{intl.getMessage('anonymize_client_ip_desc')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={values.anonymize_client_ip} onChange={(e) => setFieldValue('anonymize_client_ip', e)}/>
|
||||
</div>
|
||||
<div className={s.item}>
|
||||
<div>
|
||||
<div className={s.nameTitle}>
|
||||
{intl.getMessage('query_log_retention')}
|
||||
</div>
|
||||
<div className={s.nameDesc}>
|
||||
{intl.getMessage('query_log_retention_confirm')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Group value={values.interval} onChange={(e) => setFieldValue('interval', e.target.value)}>
|
||||
<Radio value={1} className={s.radio}>
|
||||
{intl.getMessage('interval_24_hour')}
|
||||
</Radio>
|
||||
<Radio value={7} className={s.radio}>
|
||||
{intl.getPlural('interval_days', 7, { count: 7 })}
|
||||
</Radio>
|
||||
<Radio value={30} className={s.radio}>
|
||||
{intl.getPlural('interval_days', 30, { count: 30 })}
|
||||
</Radio>
|
||||
<Radio value={90} className={s.radio}>
|
||||
{intl.getPlural('interval_days', 90, { count: 90 })}
|
||||
</Radio>
|
||||
</Group>
|
||||
{dirty && (
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
className={s.save}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{intl.getMessage('save_btn')}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default QueryLog;
|
||||
@@ -0,0 +1,105 @@
|
||||
import React, { FC, useContext, useState } from 'react';
|
||||
import { Radio, Button } from 'antd';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { notifySuccess, ConfirmModalLayout } from 'Common/ui';
|
||||
import { IStatsConfig } from 'Entities/StatsConfig';
|
||||
import Store from 'Store';
|
||||
|
||||
import { s } from '.';
|
||||
|
||||
const { Group } = Radio;
|
||||
|
||||
const Statistics: FC = observer(() => {
|
||||
const store = useContext(Store);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const { ui: { intl }, generalSettings } = store;
|
||||
const {
|
||||
statsConfig,
|
||||
} = generalSettings;
|
||||
|
||||
const onSubmit = async (values: IStatsConfig, helpers: FormikHelpers<IStatsConfig>) => {
|
||||
await generalSettings.updateStatsConfig(values);
|
||||
helpers.setSubmitting(false);
|
||||
};
|
||||
|
||||
const onReset = async () => {
|
||||
const result = await generalSettings.statsReset();
|
||||
if (result) {
|
||||
notifySuccess(intl.getMessage('stats_reset'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={s.title}>
|
||||
{intl.getMessage('statistics_configuration')}
|
||||
<Button onClick={() => setShowConfirm(true)}>
|
||||
{intl.getMessage('statistics_clear')}
|
||||
</Button>
|
||||
</div>
|
||||
<ConfirmModalLayout
|
||||
visible={showConfirm}
|
||||
onConfirm={onReset}
|
||||
onClose={() => setShowConfirm(false)}
|
||||
title={intl.getMessage('statistics_clear')}
|
||||
buttonText={intl.getMessage('statistics_clear')}
|
||||
>
|
||||
{intl.getMessage('statistics_clear_confirm')}
|
||||
</ConfirmModalLayout>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={statsConfig!.serialize()}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({
|
||||
handleSubmit,
|
||||
values,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
dirty,
|
||||
}) => (
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
<div className={s.item}>
|
||||
<div>
|
||||
<div className={s.nameTitle}>
|
||||
{intl.getMessage('statistics_retention')}
|
||||
</div>
|
||||
<div className={s.nameDesc}>
|
||||
{intl.getMessage('statistics_retention_desc')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Group value={values.interval} onChange={(e) => setFieldValue('interval', e.target.value)}>
|
||||
<Radio value={1} className={s.radio}>
|
||||
{intl.getMessage('interval_24_hour')}
|
||||
</Radio>
|
||||
<Radio value={7} className={s.radio}>
|
||||
{intl.getPlural('interval_days', 7, { count: 7 })}
|
||||
</Radio>
|
||||
<Radio value={30} className={s.radio}>
|
||||
{intl.getPlural('interval_days', 30, { count: 30 })}
|
||||
</Radio>
|
||||
<Radio value={90} className={s.radio}>
|
||||
{intl.getPlural('interval_days', 90, { count: 90 })}
|
||||
</Radio>
|
||||
</Group>
|
||||
{dirty && (
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
className={s.save}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{intl.getMessage('save_btn')}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Statistics;
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as General } from './General';
|
||||
export { default as QueryLog } from './QueryLog';
|
||||
export { default as Statistics } from './Statistics';
|
||||
export enum TAB_KEY {
|
||||
GENERAL = 'GENERAL',
|
||||
QUERY_LOG = 'QUERY_LOG',
|
||||
STATISTICS = 'STATISTICS',
|
||||
}
|
||||
export { default as s } from './Common.module.pcss';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as GeneralSettings } from './GeneralSettings';
|
||||
1
client2/src/components/App/Settings/index.ts
Normal file
1
client2/src/components/App/Settings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { GeneralSettings } from './GeneralSettings';
|
||||
31
client2/src/components/App/SetupGuide/SetupGuide.module.pcss
Normal file
31
client2/src/components/App/SetupGuide/SetupGuide.module.pcss
Normal 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);
|
||||
}
|
||||
92
client2/src/components/App/SetupGuide/SetupGuide.tsx
Normal file
92
client2/src/components/App/SetupGuide/SetupGuide.tsx
Normal 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;
|
||||
1
client2/src/components/App/SetupGuide/index.tsx
Normal file
1
client2/src/components/App/SetupGuide/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './SetupGuide';
|
||||
23
client2/src/components/App/Sidebar/Sidebar.module.pcss
Normal file
23
client2/src/components/App/Sidebar/Sidebar.module.pcss
Normal 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;
|
||||
}
|
||||
}
|
||||
116
client2/src/components/App/Sidebar/Sidebar.tsx
Normal file
116
client2/src/components/App/Sidebar/Sidebar.tsx
Normal 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;
|
||||
1
client2/src/components/App/Sidebar/index.ts
Normal file
1
client2/src/components/App/Sidebar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Sidebar';
|
||||
1
client2/src/components/App/index.ts
Normal file
1
client2/src/components/App/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './App';
|
||||
@@ -2,9 +2,10 @@ import React, { FC } from 'react';
|
||||
import { Layout } from 'antd';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { IInitialConfigurationBeta } from 'Entities/InitialConfigurationBeta';
|
||||
import Icons from 'Lib/theme/Icons';
|
||||
import Icons from 'Common/ui/Icons';
|
||||
import {
|
||||
DEFAULT_DNS_ADDRESS,
|
||||
DEFAULT_DNS_PORT,
|
||||
@@ -109,8 +110,8 @@ const InstallForm: FC = observer(() => {
|
||||
|
||||
const Install: FC = () => {
|
||||
return (
|
||||
<Layout className={theme.install.layout}>
|
||||
<Content className={theme.install.container}>
|
||||
<Layout className={cn(theme.content.content, theme.content.content_auth)}>
|
||||
<Content className={cn(theme.content.container, theme.content.container_auth)}>
|
||||
<InstallForm />
|
||||
</Content>
|
||||
<Icons/>
|
||||
|
||||
@@ -3,10 +3,11 @@ import { Tabs, Grid } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { FormikHelpers } from 'formik';
|
||||
|
||||
import { DHCP_LINK } from 'Consts/common';
|
||||
import { danger, externalLink, p } from 'Common/formating';
|
||||
import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/install';
|
||||
import Store from 'Store/installStore';
|
||||
import theme from 'Lib/theme';
|
||||
import { danger, p } from 'Common/formating';
|
||||
import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/install';
|
||||
|
||||
import { FormValues } from '../../Install';
|
||||
import StepButtons from '../StepButtons';
|
||||
@@ -26,17 +27,6 @@ const ConfigureDevices: FC<ConfigureDevicesProps> = ({
|
||||
const screens = useBreakpoint();
|
||||
const tabsPosition = screens.md ? 'left' : 'top';
|
||||
|
||||
const dhcp = (e: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={theme.link.link}
|
||||
>
|
||||
{e}
|
||||
</a>
|
||||
);
|
||||
|
||||
const allIps = addresses?.interfaces.reduce<string[]>((all, data) => {
|
||||
const { ipAddresses } = data;
|
||||
if (ipAddresses) {
|
||||
@@ -138,7 +128,7 @@ const ConfigureDevices: FC<ConfigureDevicesProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_configure_dhcp', { dhcp })}
|
||||
{intl.getMessage('install_configure_dhcp', { dhcp: externalLink(DHCP_LINK) })}
|
||||
</div>
|
||||
<StepButtons
|
||||
setFieldValue={setFieldValue}
|
||||
|
||||
67
client2/src/components/common/controls/Button/Button.tsx
Normal file
67
client2/src/components/common/controls/Button/Button.tsx
Normal 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;
|
||||
1
client2/src/components/common/controls/Button/index.ts
Normal file
1
client2/src/components/common/controls/Button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Button';
|
||||
@@ -6,7 +6,7 @@ import s from './Radio.module.pcss';
|
||||
|
||||
const { Group } = Radio;
|
||||
|
||||
interface AdminInterfaceProps {
|
||||
interface RadioProps {
|
||||
options: {
|
||||
label: string;
|
||||
desc?: string;
|
||||
@@ -16,7 +16,7 @@ interface AdminInterfaceProps {
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
const RadioComponent: FC<AdminInterfaceProps> = observer(({
|
||||
const RadioComponent: FC<RadioProps> = observer(({
|
||||
options, onSelect, value,
|
||||
}) => {
|
||||
if (options.length === 0) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as Radio } from './Radio';
|
||||
export { Input } from './Input';
|
||||
export { Switch } from './Switch';
|
||||
export { default as Button } from './Button';
|
||||
|
||||
12
client2/src/components/common/formating/code.tsx
Normal file
12
client2/src/components/common/formating/code.tsx
Normal 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;
|
||||
13
client2/src/components/common/formating/externalLink.tsx
Normal file
13
client2/src/components/common/formating/externalLink.tsx
Normal 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>
|
||||
);
|
||||
@@ -1,2 +1,4 @@
|
||||
export { default as danger } from './danger';
|
||||
export { default as p } from './p';
|
||||
export { default as code } from './code';
|
||||
export { externalLink } from './externalLink';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { IconType } from 'Lib/theme/Icons';
|
||||
import { IconType } from 'Common/ui/Icons';
|
||||
|
||||
import s from './Icon.module.pcss';
|
||||
|
||||
@@ -22,4 +22,4 @@ const Icon: FC<IconProps> = ({ icon, color, className, onClick }) => {
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
export { IconType } from 'Lib/theme/Icons';
|
||||
export { IconType } from 'Common/ui/Icons';
|
||||
|
||||
3
client2/src/components/common/ui/Icons/Icon.pcss
Normal file
3
client2/src/components/common/ui/Icons/Icon.pcss
Normal file
@@ -0,0 +1,3 @@
|
||||
.icons {
|
||||
display: none;
|
||||
}
|
||||
84
client2/src/components/common/ui/Icons/index.tsx
Normal file
84
client2/src/components/common/ui/Icons/index.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { FC } from 'react';
|
||||
import './Icon.pcss';
|
||||
|
||||
export type IconType =
|
||||
'logo' |
|
||||
'visibility_disable' |
|
||||
'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">
|
||||
<symbol id="logo" viewBox="0 0 185 57">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path fill="#242424" fillRule="nonzero" d="M0.5,15.7348066 L7.2386844,0.154696133 L10.4283283,0.154696133 L17.1670127,15.7348066 L13.5505855,15.7348066 L12.1129994,12.2651934 L5.46416417,12.2651934 L4.02657817,15.7348066 L0.5,15.7348066 Z M6.69958965,9.25966851 L10.877574,9.25966851 L8.78858181,4.24309392 L6.69958965,9.25966851 Z M18.9722792,15.7348066 L18.9722792,0.26519337 L25.104482,0.26519337 C27.5603704,0.26519337 29.5669808,0.99815105 31.1243734,2.4640884 C32.681766,3.93002575 33.4604507,5.77531116 33.4604507,8 C33.4604507,10.2099558 32.6780224,12.0515654 31.1131423,13.5248619 C29.5482622,14.9981584 27.5453955,15.7348066 25.104482,15.7348066 L18.9722792,15.7348066 Z M22.4314705,12.6629834 L25.104482,12.6629834 C26.5271003,12.6629834 27.6726695,12.2320485 28.5412111,11.3701657 C29.4097569,10.508283 29.8440234,9.38490564 29.8440234,8 C29.8440234,6.6298274 29.4060133,5.51013326 28.5299799,4.64088398 C27.6539466,3.7716347 26.5121254,3.33701657 25.104482,3.33701657 L22.4314705,3.33701657 L22.4314705,12.6629834 Z M43.4869121,16 C41.0459987,16 39.0581066,15.2486263 37.5231764,13.7458564 C35.9882462,12.2430864 35.2207926,10.3278201 35.2207926,8 C35.2207926,5.77531116 36.0069646,3.88582729 37.5793321,2.33149171 C39.1516997,0.777156133 41.113386,0 43.4644498,0 C44.8271684,0 45.9802206,0.173110608 46.9236412,0.519337017 C47.8670617,0.865563425 48.7730313,1.39962807 49.6415772,2.12154696 L47.4627359,4.70718232 C46.803839,4.16205989 46.1674141,3.76427381 45.553442,3.51381215 C44.9394699,3.2633505 44.2057051,3.13812155 43.3521384,3.13812155 C42.0942444,3.13812155 41.0272967,3.61325492 40.1512633,4.56353591 C39.27523,5.51381691 38.8372199,6.65929348 38.8372199,8 C38.8372199,9.41437171 39.2827173,10.5856307 40.1737256,11.5138122 C41.0647339,12.4419936 42.2065551,12.9060773 43.5992235,12.9060773 C44.8870674,12.9060773 45.9727335,12.5966882 46.8562543,11.9779006 L46.8562543,9.7679558 L43.3746007,9.7679558 L43.3746007,6.82872928 L50.2031342,6.82872928 L50.2031342,13.5469613 C48.2414185,15.1823286 46.0027002,16 43.4869121,16 Z M60.086538,15.9779006 C57.9451232,15.9779006 56.2754376,15.392271 55.0774493,14.2209945 C53.8794549,13.0497179 53.2804668,11.3443943 53.2804668,9.10497238 L53.2804668,0.26519337 L56.7396581,0.26519337 L56.7396581,9.01657459 C56.7396581,10.2541498 57.0354085,11.2007334 57.6269182,11.8563536 C58.2184279,12.5119738 59.0532677,12.839779 60.1314626,12.839779 C61.2096575,12.839779 62.0444972,12.5230234 62.6360069,11.8895028 C63.2275166,11.2559821 63.5232671,10.335181 63.5232671,9.12707182 L63.5232671,0.26519337 L66.9824584,0.26519337 L66.9824584,8.99447514 C66.9824584,11.2928292 66.3722392,13.0313017 65.1517825,14.2099448 C63.9313257,15.3885878 62.2429278,15.9779006 60.086538,15.9779006 Z M67.9199798,15.7348066 L74.6586642,0.154696133 L77.8483082,0.154696133 L84.5869926,15.7348066 L80.9705653,15.7348066 L79.5329793,12.2651934 L72.884144,12.2651934 L71.446558,15.7348066 L67.9199798,15.7348066 Z M74.1195695,9.25966851 L78.2975538,9.25966851 L76.2085617,4.24309392 L74.1195695,9.25966851 Z M86.3922591,15.7348066 L86.3922591,0.26519337 L93.5801891,0.26519337 C95.5718547,0.26519337 97.0992745,0.788208398 98.1624945,1.83425414 C99.0609902,2.71823646 99.5102314,3.9115947 99.5102314,5.41436464 C99.5102314,7.78638387 98.3871285,9.38489459 96.1408892,10.2099448 L99.9819393,15.7348066 L95.9387286,15.7348066 L92.5244619,10.7845304 L89.8514504,10.7845304 L89.8514504,15.7348066 L86.3922591,15.7348066 Z M89.8514504,7.77900552 L93.3555663,7.77900552 C94.1941623,7.77900552 94.8455619,7.57642928 95.3097847,7.17127072 C95.7740075,6.76611215 96.0061155,6.2246811 96.0061155,5.54696133 C96.0061155,4.82504243 95.7665202,4.27624497 95.2873225,3.90055249 C94.8081247,3.52486 94.1417504,3.33701657 93.2881794,3.33701657 L89.8514504,3.33701657 L89.8514504,7.77900552 Z M102.011829,15.7348066 L102.011829,0.26519337 L108.144031,0.26519337 C110.59992,0.26519337 112.60653,0.99815105 114.163923,2.4640884 C115.721315,3.93002575 116.5,5.77531116 116.5,8 C116.5,10.2099558 115.717572,12.0515654 114.152692,13.5248619 C112.587812,14.9981584 110.584945,15.7348066 108.144031,15.7348066 L102.011829,15.7348066 Z M105.47102,12.6629834 L108.144031,12.6629834 C109.56665,12.6629834 110.712219,12.2320485 111.58076,11.3701657 C112.449306,10.508283 112.883573,9.38490564 112.883573,8 C112.883573,6.6298274 112.445563,5.51013326 111.569529,4.64088398 C110.693496,3.7716347 109.551675,3.33701657 108.144031,3.33701657 L105.47102,3.33701657 L105.47102,12.6629834 Z" transform="translate(67.5 14)" />
|
||||
<path fill="#68BC71" d="M28.4993695,0 C19.5913422,0 8.84603419,2.043769 8.73987156e-06,6.54224924 C8.73987156e-06,16.2577508 -0.122097033,40.4620061 28.4993695,57 C57.1214688,40.4620061 56.9999957,16.2577508 56.9999957,6.54224924 C48.1533375,2.043769 37.4080296,0 28.4993695,0 L28.4993695,0 Z" />
|
||||
<path fill="#67B279" d="M28.4993695,0 L28.4993695,57 C0.736546964,40.9581459 0.0185516086,17.7031064 0.000458427595,7.45516583 L8.73987165e-06,6.54224924 C8.84603419,2.043769 19.5913422,0 28.4993695,0 L28.4993695,0 Z" />
|
||||
<path fill="#FFF" d="M28.2485704,36.6428571 L44.7857143,14.7312121 C43.573906,13.7763263 42.510977,14.450265 41.9258467,14.9720239 L41.9044958,14.9736962 L28.1158485,29.075123 L22.9206532,22.9288475 C20.4422167,20.113802 17.0728126,22.2610406 16.2857143,22.8285092 L28.2485704,36.6428571" />
|
||||
<path fill="#4D4D4D" fillRule="nonzero" d="M70.056,49 L70.056,43.768 L76.072,43.768 L76.072,49 L77.592,49 L77.592,37.576 L76.072,37.576 L76.072,42.488 L70.056,42.488 L70.056,37.576 L68.536,37.576 L68.536,49 L70.056,49 Z M85.92,49.256 C86.8266667,49.256 87.6213333,49.0933333 88.304,48.768 C88.9866667,48.4426667 89.5573333,48.0026667 90.016,47.448 C90.4746667,46.8933333 90.8186667,46.256 91.048,45.536 C91.2773333,44.816 91.392,44.0666667 91.392,43.288 C91.392,42.5093333 91.2773333,41.76 91.048,41.04 C90.8186667,40.32 90.4746667,39.6826667 90.016,39.128 C89.5573333,38.5733333 88.9866667,38.1306667 88.304,37.8 C87.6213333,37.4693333 86.8266667,37.304 85.92,37.304 C85.0133333,37.304 84.2186667,37.4693333 83.536,37.8 C82.8533333,38.1306667 82.2826667,38.5733333 81.824,39.128 C81.3653333,39.6826667 81.0213333,40.32 80.792,41.04 C80.5626667,41.76 80.448,42.5093333 80.448,43.288 C80.448,44.0666667 80.5626667,44.816 80.792,45.536 C81.0213333,46.256 81.3653333,46.8933333 81.824,47.448 C82.2826667,48.0026667 82.8533333,48.4426667 83.536,48.768 C84.2186667,49.0933333 85.0133333,49.256 85.92,49.256 Z M85.92,47.992 C85.2266667,47.992 84.6293333,47.856 84.128,47.584 C83.6266667,47.312 83.216,46.952 82.896,46.504 C82.576,46.056 82.3413333,45.552 82.192,44.992 C82.0426667,44.432 81.968,43.864 81.968,43.288 C81.968,42.712 82.0426667,42.144 82.192,41.584 C82.3413333,41.024 82.576,40.52 82.896,40.072 C83.216,39.624 83.6266667,39.264 84.128,38.992 C84.6293333,38.72 85.2266667,38.584 85.92,38.584 C86.6133333,38.584 87.2106667,38.72 87.712,38.992 C88.2133333,39.264 88.624,39.624 88.944,40.072 C89.264,40.52 89.4986667,41.024 89.648,41.584 C89.7973333,42.144 89.872,42.712 89.872,43.288 C89.872,43.864 89.7973333,44.432 89.648,44.992 C89.4986667,45.552 89.264,46.056 88.944,46.504 C88.624,46.952 88.2133333,47.312 87.712,47.584 C87.2106667,47.856 86.6133333,47.992 85.92,47.992 Z M95.72,49 L95.72,39.496 L95.752,39.496 L99.32,49 L100.616,49 L104.184,39.496 L104.216,39.496 L104.216,49 L105.656,49 L105.656,37.576 L103.576,37.576 L99.96,47.176 L96.36,37.576 L94.28,37.576 L94.28,49 L95.72,49 Z M117.12,49 L117.12,47.72 L110.704,47.72 L110.704,43.768 L116.64,43.768 L116.64,42.488 L110.704,42.488 L110.704,38.856 L117.072,38.856 L117.072,37.576 L109.184,37.576 L109.184,49 L117.12,49 Z" />
|
||||
</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>
|
||||
|
||||
<symbol id="visibility_enable" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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>
|
||||
);
|
||||
|
||||
export default Icons;
|
||||
@@ -0,0 +1,10 @@
|
||||
.wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 22px;
|
||||
margin-right: 10px;
|
||||
color: var(--gray700);
|
||||
}
|
||||
23
client2/src/components/common/ui/LangSelect/LangSelect.tsx
Normal file
23
client2/src/components/common/ui/LangSelect/LangSelect.tsx
Normal 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;
|
||||
1
client2/src/components/common/ui/LangSelect/index.tsx
Normal file
1
client2/src/components/common/ui/LangSelect/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './LangSelect';
|
||||
63
client2/src/components/common/ui/Link.tsx
Normal file
63
client2/src/components/common/ui/Link.tsx
Normal 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;
|
||||
26
client2/src/components/common/ui/Mask/Mask.module.pcss
Normal file
26
client2/src/components/common/ui/Mask/Mask.module.pcss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
client2/src/components/common/ui/Mask/Mask.tsx
Normal file
23
client2/src/components/common/ui/Mask/Mask.tsx
Normal 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;
|
||||
1
client2/src/components/common/ui/Mask/index.ts
Normal file
1
client2/src/components/common/ui/Mask/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Mask';
|
||||
@@ -1,2 +1,6 @@
|
||||
export { default as Icon } from './Icon';
|
||||
export { notifyError, notifySuccess } from './Notifications';
|
||||
export { default as Link } from './Link';
|
||||
export { default as LangSelect } from './LangSelect';
|
||||
export { default as Mask } from './Mask';
|
||||
export { CommonLayout, InnerLayout, CommonModalLayout, ConfirmModalLayout } from './layouts';
|
||||
|
||||
16
client2/src/components/common/ui/layouts/CommonLayout.tsx
Normal file
16
client2/src/components/common/ui/layouts/CommonLayout.tsx
Normal 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;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { FC, useContext, useEffect } from 'react';
|
||||
import { Modal, Button } from 'antd';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { Icon } from 'Common/ui';
|
||||
import Store from 'Store';
|
||||
|
||||
interface CommonModalLayoutProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
buttonText?: string;
|
||||
className?: string;
|
||||
width?: number;
|
||||
onClose: () => void;
|
||||
onSubmit?: () => void;
|
||||
noFooter?: boolean;
|
||||
disabled?: boolean;
|
||||
centered?: boolean;
|
||||
}
|
||||
|
||||
const CommonModalLayout: FC<CommonModalLayoutProps> = ({
|
||||
visible,
|
||||
children,
|
||||
title,
|
||||
buttonText,
|
||||
className,
|
||||
width,
|
||||
onClose,
|
||||
onSubmit,
|
||||
noFooter,
|
||||
disabled,
|
||||
centered,
|
||||
}) => {
|
||||
const store = useContext(Store);
|
||||
const { ui: { intl } } = store;
|
||||
|
||||
useEffect(() => {
|
||||
const onEnter = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && onSubmit) {
|
||||
onSubmit();
|
||||
}
|
||||
};
|
||||
if (onSubmit) {
|
||||
window.addEventListener('keyup', onEnter);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('keyup', onEnter);
|
||||
};
|
||||
}, [onSubmit]);
|
||||
const footer = noFooter ? null : [
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
key="submit"
|
||||
htmlType="submit"
|
||||
onClick={onSubmit}
|
||||
disabled={disabled}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>,
|
||||
<Button
|
||||
type="link"
|
||||
size="large"
|
||||
key="cancel"
|
||||
onClick={onClose}
|
||||
>
|
||||
{intl.getMessage('cancel')}
|
||||
</Button>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={title}
|
||||
wrapClassName={cn('modal', className)}
|
||||
onCancel={onClose}
|
||||
footer={footer}
|
||||
closeIcon={<Icon icon="close_big" />}
|
||||
width={width || 480}
|
||||
centered={centered}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonModalLayout;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import CommonModalLayout from './CommonModalLayout';
|
||||
|
||||
interface DeleteModalLayoutProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
buttonText: string;
|
||||
onClose: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
const DeleteModalLayout: FC<DeleteModalLayoutProps> = ({
|
||||
visible,
|
||||
children,
|
||||
title,
|
||||
buttonText,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}) => {
|
||||
return (
|
||||
<CommonModalLayout
|
||||
visible={visible}
|
||||
title={title}
|
||||
buttonText={buttonText}
|
||||
onSubmit={onConfirm}
|
||||
onClose={onClose}
|
||||
>
|
||||
{children}
|
||||
</CommonModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteModalLayout;
|
||||
41
client2/src/components/common/ui/layouts/InnerLayout.tsx
Normal file
41
client2/src/components/common/ui/layouts/InnerLayout.tsx
Normal 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;
|
||||
4
client2/src/components/common/ui/layouts/index.ts
Normal file
4
client2/src/components/common/ui/layouts/index.ts
Normal 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';
|
||||
Reference in New Issue
Block a user