Pull request #961: New client dashboard

Merge in DNS/adguard-home from new-client-dashboard to master

Squashed commit of the following:

commit 7bbd67c1e3d2af62b96bf41bb356cd6b784e473e
Merge: 113743a6 9cd9054c
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Wed Feb 3 16:01:17 2021 +0300

    Merge branch 'master' into new-client-dashboard

commit 113743a60665e40383d367dc17fa709dc54e4e2e
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Wed Feb 3 15:45:16 2021 +0300

    Remove unneded modal styles

commit 04f9d93a9ac17ee046f0d5bedfb2bf5a5e6c0a48
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Wed Feb 3 14:19:56 2021 +0300

    Consider comments

commit 78a96cd8fed8b3e03547e7e45724c23db295f67b
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Mon Feb 1 18:46:52 2021 +0300

    Remove old params for MiniCssExtractPlugin

commit 40e5a9b2b1e04036deb70af17f2719eadd0c9c02
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Mon Feb 1 18:27:46 2021 +0300

    Fix mobile version

commit 509cefc308f945b03cafa62bf48257490a0a4be1
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Mon Feb 1 18:20:56 2021 +0300

    Remove unneeded imports

commit d192f39cd2503b8ec942f00ba78fca02cac9fa60
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Mon Feb 1 18:20:13 2021 +0300

    Finish first version of dashboard

commit f82429e53d334874ff7dd0641092ec83c66ab61c
Merge: fd91a0a3 3e0238aa
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Mon Feb 1 17:12:59 2021 +0300

    Merge branch 'master' into new-client-dashboard

commit fd91a0a3d76c2a052a6548232b75d151d6065b88
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Mon Feb 1 17:12:27 2021 +0300

    wip

commit 237679965052d38acfcd6a72d24b2444cc5b3896
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Fri Jan 29 11:18:10 2021 +0300

    Finish general settings

commit 397a7e10efd34a8d31bb175a5a5a7158338388d4
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Thu Jan 28 19:24:03 2021 +0300

    Add General settings page

commit 486aaa6f3f9ad66f3a0dcfcccad9a32659767e90
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Thu Jan 28 14:05:16 2021 +0300

    Remove husky

commit b895306c0655019ca56ce161e050d83b4e7f5ff1
Merge: a195f1f4 154c9c1c
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Thu Jan 28 14:03:37 2021 +0300

    Merge branch 'master' into new-client-dashboard

commit a195f1f4d46043d9c53dea08734733f9817b95a0
Merge: c45c5fe9 362f390f
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Wed Jan 27 15:46:18 2021 +0300

    Merge branch 'new-client-dashboard' of ssh://bit.adguard.com:7999/dns/adguard-home into new-client-dashboard

commit c45c5fe92e6c5c852bec8f512dc46b4cd513156c
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Wed Jan 27 15:46:01 2021 +0300

    wip

commit 362f390fd3dcfca75633a8d30a2e54c3c30b4f3d
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Wed Jan 27 15:45:12 2021 +0300

    Pull request #949: + client: add setup guide page

    Merge in DNS/adguard-home from 2554-setup-guide to new-client-dashboard

    Squashed commit of the following:

    commit c240d52e9e5d90429f2018fde808f4d04ccec138
    Merge: 256f1056 137b88e4
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Wed Jan 27 14:13:52 2021 +0300

        Merge branch 'new-client-dashboard' into 2554-setup-guide

    commit 256f1056770c67339e93275ab6dc7aaf2c10da0b
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Wed Jan 27 14:10:45 2021 +0300

        + client: add DNS addresses to the setup guide

    commit 0ecf91275a16ecc0dca23cae2ae209836fc622d2
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Wed Jan 27 14:00:12 2021 +0300

        + client: add setup guide tabs

commit 137b88e4253af5be32d542adbe74575ef74805c8
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Thu Jan 21 19:17:58 2021 +0300

    Add clients top

commit c3318e6932d87fdff5f22d76bee12b49f099129a
Merge: 2776276b 021eb22f
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Thu Jan 21 19:15:57 2021 +0300

    Merge branch 'new-client-dashboard' of ssh://bit.adguard.com:7999/dns/adguard-home into new-client-dashboard

commit 2776276b2e6dc026e1326b02c388fcf7d48d47ff
Author: Vlad <v.abdulmyanov@adguard.com>
Date:   Thu Jan 21 19:15:53 2021 +0300

    Add top client info

commit 021eb22ff877aec12eb7fab60147a2cc2ddd08b7
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Jan 21 14:13:54 2021 +0300

    Merge: client: add sidebar

    Squashed commit of the following:

    commit 6885ba953971e68602889fbb3219221f90265421
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Thu Jan 21 13:56:55 2021 +0300

        add sidebar mask

    commit f069bfe8cba2b31355e19a51ca00bf774ee9e560
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Thu Jan 21 13:03:47 2021 +0300

        fix store

    commit 77c8791002887ae022da07dc264d9010576e7bab
    Merge: d0a6eff6 ea6d54d4
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Thu Jan 21 13:01:04 2021 +0300

        Merge branch 'new-client-dashboard' into 2254-sidebar

    commit d0a6eff67fd74533d63f5d56382085e98ddbb702
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Thu Jan 21 12:47:32 2021 +0300

        client: remove unused file

    commit 9d2424477de85503fe41fa00cc1294cb0c0e7dfa
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Thu Jan 21 12:39:13 2021 +0300

        client: header

    commit 9ddea19c136f15b184caa72d7e82738f7d4f3f1f
    Merge: 797f1248 b694bb05
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Thu Jan 21 10:57:24 2021 +0300

        Merge branch 'new-client-dashboard' into 2254-sidebar

    commit 797f1248df5c1ef8e59c2a9999138f9e05a7adaa
    Author: Ildar Kamalov <ik@adguard.com>
    Date:   Thu Jan 21 10:51:57 2021 +0300

        client: sidebar

... and 14 more commits
This commit is contained in:
Vladislav Abdulmyanov
2021-02-03 16:14:20 +03:00
parent 9cd9054cdb
commit 0c127039cf
136 changed files with 5384 additions and 2938 deletions

View File

@@ -1,16 +0,0 @@
import { observer } from 'mobx-react-lite';
import React, { FC, useContext } from 'react';
import Store from 'Store';
import Icons from 'Lib/theme/Icons';
const App: FC = observer(() => {
const store = useContext(Store);
return (
<div>
{store.ui.currentLang}
<Icons/>
</div>
);
});
export default App;

View File

@@ -0,0 +1,20 @@
import React, { FC } from 'react';
import { BrowserRouter } from 'react-router-dom';
import Icons from 'Common/ui/Icons';
import Routes from './Routes';
import { ErrorBoundary } from './Errors';
const App: FC = () => {
return (
<ErrorBoundary>
<BrowserRouter>
<Routes />
<Icons />
</BrowserRouter>
</ErrorBoundary>
);
};
export default App;

View File

@@ -0,0 +1,136 @@
import React, { FC, useContext } from 'react';
import { Row, Col } from 'antd';
import { observer } from 'mobx-react-lite';
import Store from 'Store';
import { InnerLayout } from 'Common/ui/layouts';
import theme from 'Lib/theme';
import { BlockCard, TopDomains, BlockedQueries, TopClients, ServerStatistics } from './components';
const Dashboard:FC = observer(() => {
const store = useContext(Store);
const {
dashboard: { stats, filteringConfig },
system: { status },
ui: { intl },
} = store;
if (!stats || !filteringConfig) {
return null;
}
const {
numBlockedFiltering,
numReplacedParental,
numReplacedSafebrowsing,
replacedParental,
replacedSafebrowsing,
avgProcessingTime,
blockedFiltering,
topBlockedDomains,
topQueriedDomains,
dnsQueries,
numDnsQueries,
} = stats;
const { filters } = filteringConfig!;
const allFilters = filters?.length;
const allRules = filters?.reduce((prev, e) => prev + (e.rulesCount || 0), 0);
const enabled = filters?.filter((e) => e.enabled).length;
return (
<InnerLayout title={`AdGuard Home ${status?.version}`}>
<div className={theme.content.container}>
<Row gutter={[24, 24]}>
<Col span={24} md={12}>
<TopDomains
title={intl.getMessage('stats_query_domain')}
overal={numDnsQueries!}
chartData={dnsQueries!}
tableData={topQueriedDomains!}
color={theme.chartColors.green}
/>
</Col>
<Col span={24} md={12}>
<TopDomains
useValueColor
title={intl.getMessage('top_blocked_domains')}
overal={numBlockedFiltering!}
chartData={blockedFiltering!}
tableData={topBlockedDomains!}
color={theme.chartColors.red}
/>
</Col>
</Row>
<Row gutter={[24, 24]}>
<Col span={24} md={18}>
<Row gutter={[24, 24]}>
<Col span={24} md={8}>
<BlockCard
title={intl.getMessage('dashboard_blocked_ads')}
overal={numBlockedFiltering!}
data={blockedFiltering!}
color={theme.chartColors.red}
/>
</Col>
<Col span={24} md={8}>
<BlockCard
title={intl.getMessage('dashboard_blocked_trackers')}
overal={numBlockedFiltering!}
data={blockedFiltering!}
color={theme.chartColors.orange}
/>
</Col>
<Col span={24} md={8}>
<BlockCard
title={intl.getMessage('stats_adult')}
overal={numReplacedParental!}
data={replacedParental!}
color={theme.chartColors.purple}
/>
</Col>
<Col span={24} md={8}>
<BlockCard
title={intl.getMessage('stats_malware_phishing')}
overal={numReplacedSafebrowsing!}
data={replacedSafebrowsing!}
color={theme.chartColors.red}
/>
</Col>
<Col span={24} md={8}>
<BlockCard
title={intl.getMessage('average_processing_time')}
overal={`${Math.round(avgProcessingTime! * 100)} ${intl.getMessage('milliseconds_abbreviation')}`}
data={blockedFiltering!}
color={theme.chartColors.green}
/>
</Col>
<Col span={24} md={8}>
<BlockCard
title={intl.getMessage('dashboard_filter_rules')}
overal={allRules!}
text={intl.getMessage('dashboard_filter_rules_count', { enabled, all: allFilters })}
color={theme.chartColors.green}
/>
</Col>
</Row>
</Col>
<Col span={24} md={6}>
{/* TODO: fix chart */}
<BlockedQueries
other={numBlockedFiltering! / 3}
ads={numBlockedFiltering!}
trackers={numBlockedFiltering!}
/>
</Col>
</Row>
<TopClients />
<ServerStatistics />
</div>
</InnerLayout>
);
});
export default Dashboard;

View File

@@ -0,0 +1,20 @@
.container {
display: flex;
flex-flow: column;
padding: 24px;
background-color: var(--white);
}
.title {
font-size: 14px;
line-height: 22px;
margin-bottom: 4px;
color: var(--gray700);
}
.overal {
font-size: 30px;
line-height: 38px;
margin-bottom: 18px;
color: var(--gray900);
}

View File

@@ -0,0 +1,35 @@
import React, { FC } from 'react';
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
import s from './BlockCard.module.pcss';
interface BlockCardProps {
overal: number | string;
data?: number[];
text?: string;
color?: string;
title: string;
}
const BlockCard: FC<BlockCardProps> = ({ overal, data, color, title, text }) => {
return (
<div className={s.container}>
<div className={s.title}>{title}</div>
<div className={s.overal}>{overal}</div>
{data && (
<ResponsiveContainer width="100%" height={25}>
<AreaChart data={data.map((n) => ({ name: 'data', value: n }))}>
<Area dataKey="value" stroke={color} fill={color} dot={false} />
</AreaChart>
</ResponsiveContainer>
)}
{text && (
<div>
{text}
</div>
)}
</div>
);
};
export default BlockCard;

View File

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

View File

@@ -0,0 +1,16 @@
.container {
display: flex;
flex-flow: column;
padding: 24px;
background-color: var(--white);
}
.title {
font-size: 14px;
line-height: 22px;
margin-bottom: 4px;
color: var(--gray700);
}
.pie {
padding: 34px 0px;
}

View File

@@ -0,0 +1,76 @@
import theme from 'Lib/theme';
import React, { FC, useContext, useState } from 'react';
import { PieChart, Pie, ResponsiveContainer, Sector, Cell } from 'recharts';
import Store from 'Store';
import s from './BlockedQueries.module.pcss';
interface BlockCardProps {
ads: number;
trackers: number;
other: number;
}
const renderActiveShape = (props: any): any => {
const {
cx, cy, innerRadius, outerRadius, startAngle, endAngle,
fill, payload, percent,
} = props;
return (
<g>
<text x={cx} y={cy - 11} dy={8} textAnchor="middle" fill={fill}>{payload.name}</text>
<text x={cx} y={cy + 18} dy={8} fontSize={24} textAnchor="middle" >{Math.round(percent * 100)}%</text>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius + 5}
outerRadius={outerRadius + 5}
startAngle={startAngle + 1}
endAngle={endAngle - 1}
fill={fill}
/>
</g>
);
};
const BlockedQueries: FC<BlockCardProps> = ({ ads, trackers, other }) => {
const store = useContext(Store);
const [activeIndex, setActiveIndex] = useState(0);
const { ui: { intl } } = store;
const data = [
{ name: intl.getMessage('other'), value: other, color: theme.chartColors.gray700 },
{ name: intl.getMessage('ads'), value: ads, color: theme.chartColors.red },
{ name: intl.getMessage('trackers'), value: trackers, color: theme.chartColors.orange },
];
const onChart: any = (_: any, index: number) => {
setActiveIndex(index);
};
return (
<div className={s.container}>
<div className={s.title}>{intl.getMessage('dashboard_blocked_queries')}</div>
<div className={s.pie}>
<ResponsiveContainer width="100%" height={190}>
<PieChart>
<Pie
activeIndex={activeIndex}
data={data}
dataKey="value"
nameKey="name"
innerRadius={60}
outerRadius={80}
activeShape={renderActiveShape}
onClick={onChart}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
</div>
);
};
export default BlockedQueries;

View File

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

View File

@@ -0,0 +1,46 @@
.container {
display: flex;
flex-flow: column;
background-color: var(--white);
margin-top: 24px;
}
.title {
padding: 24px;
font-size: 16px;
font-weight: 500;
line-height: 22px;
border-bottom: 1px solid var(--gray300);
color: var(--gray900);
}
.card {
padding: 24px;
height: 100%;
}
.cardBorder {
border-right: 1px solid var(--gray300);
&:last-of-type {
border-right: 0;
}
}
.cardTitle {
font-weight: 500;
margin-bottom: 12px;
}
.cardDesc {
color: var(--gray700);
}
.cardValue {
color: var(--gray900);
font-size: 30px;
}
.chart {
margin-top: 24px;
}

View File

@@ -0,0 +1,89 @@
import React, { FC, useContext } from 'react';
import { Row, Col } from 'antd';
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
import Store from 'Store';
import theme from 'Lib/theme';
import s from './ServerStatistics.module.pcss';
const ServerStatistics: FC = () => {
const store = useContext(Store);
const { ui: { intl } } = store;
const data = [0, 10, 2, 14, 12, 24, 5, 8, 10, 0, 3, 5, 7, 8, 3];
return (
<div className={s.container}>
<div className={s.title}>{intl.getMessage('dashboard_server_statistics')}</div>
<Row>
<Col span={24} md={6} className={s.cardBorder}>
<div className={s.card}>
<div className={s.cardTitle}>
Average server load
</div>
<div className={s.cardDesc}>
<div>
Processes: 213
</div>
<div>
Cores: 2
</div>
</div>
<ResponsiveContainer width="100%" height={25} className={s.chart}>
<AreaChart data={data.map((n) => ({ name: 'data', value: n }))}>
<Area dataKey="value" stroke={theme.chartColors.green} fill={theme.chartColors.green} dot={false} />
</AreaChart>
</ResponsiveContainer>
</div>
</Col>
<Col span={24} md={6} className={s.cardBorder}>
<div className={s.card}>
<div className={s.cardTitle}>
Memory usage
</div>
<div className={s.cardValue}>
236 Mb
</div>
<ResponsiveContainer width="100%" height={25} className={s.chart}>
<AreaChart data={data.map((n) => ({ name: 'data', value: n }))}>
<Area dataKey="value" stroke={theme.chartColors.orange} fill={theme.chartColors.orange} dot={false} />
</AreaChart>
</ResponsiveContainer>
</div>
</Col>
<Col span={24} md={6} className={s.cardBorder}>
<div className={s.card}>
<div className={s.cardTitle}>
DNS cashe size
</div>
<div className={s.cardValue}>
2 363 records
</div>
<div className={s.cardDesc}>
<div>
32 Mb
</div>
</div>
</div>
</Col>
<Col span={24} md={6} className={s.cardBorder}>
<div className={s.card}>
<div className={s.cardTitle}>
Upstream servers data
</div>
<div className={s.cardDesc}>
<div>
Processes: 213
</div>
<div>
Cores: 2
</div>
</div>
</div>
</Col>
</Row>
</div>
);
};
export default ServerStatistics;

View File

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

View File

@@ -0,0 +1,43 @@
.container {
display: flex;
flex-flow: column;
background-color: var(--white);
}
.title {
font-size: 16px;
line-height: 22px;
margin-bottom: 4px;
padding: 24px;
color: var(--gray900);
}
.table {
position: relative;
}
.tableTitle {
color: var(--gray700);
background-color: #fafafa;
padding: 24px;
position: sticky;
top: 0;
}
.tableGrid {
display: grid;
grid-template-columns: 4fr 1fr 1fr 1.5fr 1fr .5fr;
padding: 16px 24px;
border-bottom: 1px solid var(--gray300);
&:last-of-type {
border-bottom: 0;
}
> div {
align-self: center;
}
}
.ids {
color: var(--gray700)
}

View File

@@ -0,0 +1,71 @@
import React, { FC, useContext } from 'react';
import { Button } from 'antd';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import Store from 'Store';
import s from './TopClients.module.pcss';
const TopClients: FC = observer(() => {
const store = useContext(Store);
const { ui: { intl }, dashboard } = store;
const { clientsInfo, stats } = dashboard;
const topClients = new Map();
stats?.topClients?.forEach((client) => {
const [id, requests] = Object.entries(client.numberData);
topClients.set(id, requests);
});
const clients = Array.from(clientsInfo.entries());
return (
<div className={s.container}>
<div className={s.title}>{intl.getMessage('Top Clients')}</div>
<div className={s.table}>
<div className={cn(s.tableTitle, s.tableGrid)}>
<div>{intl.getMessage('client_table_header')}</div>
<div>{intl.getMessage('requests')}</div>
<div>{intl.getMessage('show_blocked_responses')}</div>
<div>%</div>
<div/>
<div/>
</div>
{clients.map(([id, c]) => {
const request = topClients.get(id);
return (
<div className={s.tableGrid} key={id}>
<div>
{c.name}
<div className={s.ids}>
{c.ids?.map((cid) => (
<div key={cid}>{cid}</div>
))}
</div>
</div>
<div>
{request}
</div>
<div>
API
{/* TODO: api */}
</div>
<div>
API / {request}
</div>
<div>
<Button>
{intl.getMessage('Block')}
</Button>
</div>
<div>
...
</div>
</div>
);
})}
</div>
</div>
);
});
export default TopClients;

View File

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

View File

@@ -0,0 +1,62 @@
.container {
display: flex;
flex-flow: column;
background-color: var(--white);
}
.title {
padding: 24px;
font-size: 16px;
font-weight: 500;
line-height: 22px;
border-bottom: 1px solid var(--gray300);
margin-bottom: 16px;
color: var(--gray900);
}
.content {
padding: 24px;
}
.overal {
font-size: 24px;
line-height: 32px;
margin-bottom: 24px;
color: var(--gray900);
}
.table {
position: relative;
overflow-y: auto;
max-height: 280px;
width: 100%;
}
.tableHeader {
/* TODO: color */
position: sticky;
top: 0;
width: inherit;
background-color: #fafafa;
font-weight: 500;
z-index: 10;
}
.tableRow {
display: grid;
grid-template-columns: 3fr 1fr 1.5fr;
grid-column-gap: 10px;
padding: 8px 16px;
border-bottom: 1px solid var(--gray300);
}
.domain {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.progress {
display: flex;
}

View File

@@ -0,0 +1,73 @@
import React, { FC, useContext } from 'react';
import { Progress } from 'antd';
import cn from 'classnames';
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
import TopArrayEntry from 'Entities/TopArrayEntry';
import theme from 'Lib/theme';
import Store from 'Store';
import s from './TopDomains.module.pcss';
interface TopDomainsProps {
title: string;
overal: number;
chartData: number[];
tableData: TopArrayEntry[];
color: string;
useValueColor?: boolean;
}
const TopDomains: FC<TopDomainsProps> = (
{ title, overal, chartData, tableData, color, useValueColor },
) => {
const store = useContext(Store);
const { ui: { intl } } = store;
const data = tableData.map((e) => {
const [domain, value] = Object.entries(e.numberData)[0];
return { domain, value };
});
return (
<div className={s.container}>
<div className={s.title}>{title}</div>
<div className={s.content}>
<div className={s.overal}>
{overal.toLocaleString('en')}
<ResponsiveContainer width="100%" height={45}>
<AreaChart data={chartData.map((n) => ({ name: 'data', value: n }))}>
<Area dataKey="value" stroke={color} fill={color} dot={false} strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</div>
<div className={s.table}>
<div className={cn(s.tableHeader, s.tableRow)}>
<div>
{intl.getMessage('domain')}
</div>
<div>
{intl.getMessage('all_queries')}
</div>
<div>
%
</div>
</div>
{data.map(({ domain, value }) => (
<div className={s.tableRow} key={domain}>
<div className={s.domain}>{domain}</div>
<div style={{ color: useValueColor ? color : 'initial' }}>{value}</div>
<Progress
percent={Math.round((value / overal) * 100)}
strokeLinecap="square"
strokeColor={theme.chartColors.gray700}
trailColor={theme.chartColors.gray300}
/>
</div>
))}
</div>
</div>
</div>
);
};
export default TopDomains;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
import React, { Component, ReactNode } from 'react';
import cn from 'classnames';
import s from './Errors.module.pcss';
export default class ErrorBoundary extends Component {
state = {
isError: false,
};
static getDerivedStateFromError(): { isError: boolean } {
return { isError: true };
}
render(): ReactNode {
const { isError } = this.state;
const { children } = this.props;
if (isError) {
return (
<div className={cn(s.content, s.content_boundary)}>
<div className={s.title}>
Something went wrong
</div>
</div>
);
}
return children;
}
}

View File

@@ -0,0 +1,79 @@
.content {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
max-width: 455px;
min-height: calc(100vh - var(--header-height) - 64px);
margin: 0 auto;
text-align: center;
&_boundary {
min-height: 100vh;
}
}
.title {
margin-bottom: 8px;
font-size: 18px;
font-weight: 500;
@media (--s-viewport) {
margin-bottom: 20px;
font-size: 24px;
}
}
.code {
position: relative;
margin-bottom: 32px;
font-size: 120px;
font-weight: 700;
line-height: 108px;
color: var(--morning);
user-select: none;
@media (--s-viewport) {
margin-bottom: 54px;
font-size: 180px;
line-height: 162px;
}
}
.warning {
width: 160px;
height: 173px;
@media (--s-viewport) {
width: 243px;
height: 262px;
}
&_code {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
@media (--s-viewport) {
top: -34px;
}
}
}
.error {
margin-bottom: 10px;
cursor: pointer;
}
.desc {
margin-bottom: 8px;
max-width: 384px;
font-size: 13px;
color: var(--gray);
@media (--s-viewport) {
margin-bottom: 20px;
font-size: 14px;
}
}

View File

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

View File

@@ -0,0 +1,81 @@
.header {
position: relative;
z-index: 1;
color: var(--gray900);
background-color: var(--white);
box-shadow: 0 1px 4px 0 rgba(0, 21, 41, 0.12);
}
.top,
.bottom {
padding: 12px 16px;
@media (--l-viewport) {
padding: 12px 32px;
}
}
.top {
background-color: var(--black);
@media (--l-viewport) {
display: none;
}
}
.bottom {
display: flex;
flex-direction: column;
@media (--l-viewport) {
align-items: center;
flex-direction: row;
height: var(--header-height);
}
}
.icon {
margin-right: 10px;
}
.status {
display: flex;
align-items: center;
margin-bottom: 12px;
@media (--l-viewport) {
margin: 0 16px 0 0;
}
}
.action {
min-width: 80px;
margin-right: auto;
}
.languages,
.user {
display: none;
@media (--l-viewport) {
display: flex;
align-items: center;
}
}
.user {
margin-right: 32px;
}
.menu {
color: var(--white);
background-color: transparent;
border: 0;
&:hover,
&:focus,
&:active {
color: var(--gray400);
background-color: transparent;
}
}

View File

@@ -0,0 +1,60 @@
import React, { FC, useContext } from 'react';
import { Button } from 'antd';
import { MenuOutlined } from '@ant-design/icons';
import { observer } from 'mobx-react-lite';
import { Icon, LangSelect } from 'Common/ui';
import Store from 'Store';
import s from './Header.module.pcss';
const Header: FC = observer(() => {
const store = useContext(Store);
const { ui: { intl }, system, ui } = store;
const { status, profile } = system;
const updateServerStatus = () => {
system.switchServerStatus(!status?.protectionEnabled);
};
return (
<div className={s.header}>
<div className={s.top}>
<Button
icon={<MenuOutlined />}
className={s.menu}
onClick={() => ui.toggleSidebar()}
/>
</div>
<div className={s.bottom}>
<div className={s.status}>
<Icon icon="logo_shield" className={s.icon} />
{status?.protectionEnabled
? intl.getMessage('header_adguard_status_enabled')
: intl.getMessage('header_adguard_status_disabled')}
</div>
<Button
type="ghost"
size="small"
className={s.action}
onClick={updateServerStatus}
>
{status?.protectionEnabled
? intl.getMessage('disable')
: intl.getMessage('enable')}
</Button>
{profile?.name && (
<div className={s.user}>
<Icon icon="user" className={s.icon} />
{profile?.name}
</div>
)}
<div className={s.languages}>
<LangSelect />
</div>
</div>
</div>
);
});
export default Header;

View File

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

View File

@@ -0,0 +1,65 @@
import React, { FC, useContext } from 'react';
import { Button } from 'antd';
import cn from 'classnames';
import { CommonLayout } from 'Common/ui/layouts';
import { code } from 'Common/formating';
import { Link } from 'Common/ui';
import Store from 'Store';
import theme from 'Lib/theme';
import s from './Login.module.pcss';
import { RoutePath } from '../Routes/Paths';
const ForgotPassword: FC = () => {
const store = useContext(Store);
const { ui: { intl } } = store;
return (
<CommonLayout className={cn(theme.content.content, theme.content.content_auth)}>
<div className={cn(theme.content.container, theme.content.container_auth)}>
<div className={s.title}>
{intl.getMessage('login_password_title')}
</div>
<p className={s.paragraph}>
{intl.getMessage('login_password_hash')}
</p>
<div className={s.list}>
<div className={s.step}>
{intl.getMessage('login_password_step_1')}
</div>
<div className={s.step}>
{intl.getMessage('login_password_step_2', { code })}
</div>
<div className={s.step}>
{intl.getMessage('login_password_step_3', { code })}
</div>
<div className={s.step}>
{intl.getMessage('login_password_step_4')}
</div>
<div className={s.step}>
{intl.getMessage('login_password_step_5')}
</div>
</div>
<p className={s.paragraph}>
{intl.getMessage('login_password_result')}
</p>
<Link to={RoutePath.Login}>
<Button
type="primary"
size="large"
block
>
{intl.getMessage('back')}
</Button>
</Link>
</div>
</CommonLayout>
);
};
export default ForgotPassword;

View File

@@ -0,0 +1,34 @@
.title {
margin-bottom: 12px;
font-size: 28px;
text-align: center;
&_form {
margin-bottom: 32px;
}
}
.link {
display: inline-block;
vertical-align: middle;
margin-top: 32px;
font-size: 16px;
text-align: center;
}
.paragraph {
font-size: 16px;
margin: 0 0 14px;
}
.list {
margin-bottom: 16px;
padding-left: 20px;
font-size: 16px;
}
.step {
margin-bottom: 5px;
display: list-item;
list-style: decimal;
}

View File

@@ -0,0 +1,102 @@
import React, { FC, useContext } from 'react';
import { Button } from 'antd';
import { observer } from 'mobx-react-lite';
import { Formik, FormikHelpers } from 'formik';
import cn from 'classnames';
import { Input } from 'Common/controls';
import { CommonLayout } from 'Common/ui/layouts';
import { Link } from 'Common/ui';
import { RoutePath } from 'Components/App/Routes/Paths';
import Store from 'Store';
import theme from 'Lib/theme';
import s from './Login.module.pcss';
type FormValues = {
name: string;
password: string;
};
const Login: FC = observer(() => {
const store = useContext(Store);
const { ui: { intl }, login } = store;
const onSubmit = async (values: FormValues, { setSubmitting }: FormikHelpers<FormValues>) => {
const { name, password } = values;
const error = await login.login({
name,
password,
});
if (error) {
setSubmitting(false);
}
};
const initialValues: FormValues = {
name: '',
password: '',
};
return (
<CommonLayout className={cn(theme.content.content, theme.content.content_auth)}>
<div className={cn(theme.content.container, theme.content.container_auth)}>
<div className={cn(s.title, s.title_form)}>
{intl.getMessage('login')}
</div>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
>
{({
values,
handleSubmit,
setFieldValue,
isSubmitting,
}) => (
<form noValidate onSubmit={handleSubmit}>
<Input
name="name"
type="text"
placeholder={intl.getMessage('username')}
value={values.name}
onChange={(v) => setFieldValue('name', v)}
autoFocus
/>
<Input
name="password"
type="password"
placeholder={intl.getMessage('password')}
value={values.password}
onChange={(v) => setFieldValue('password', v)}
/>
<Button
type="primary"
size="large"
htmlType="submit"
disabled={!values.name || !values.password || isSubmitting}
block
>
{intl.getMessage('sign_in')}
</Button>
</form>
)}
</Formik>
<div className={theme.text.center}>
<Link
to={RoutePath.ForgotPassword}
className={cn(theme.link.link, theme.link.gray, s.link)}
>
{intl.getMessage('login_password_link')}
</Link>
</div>
</div>
</CommonLayout>
);
});
export default Login;

View File

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

View File

@@ -0,0 +1,63 @@
import qs from 'qs';
import { Locale } from 'Localization';
const BasicPath = '/';
const pathBuilder = (path: string) => (`${BasicPath}${path}`);
export enum RoutePath {
Dashboard = 'Dashboard',
FiltersBlocklist = 'FiltersBlocklist',
FiltersAllowlist = 'FiltersAllowlist',
FiltersRewrites = 'FiltersRewrites',
FiltersServices = 'FiltersServices',
FiltersCustom = 'FiltersCustom',
QueryLog = 'QueryLog',
SetupGuide = 'SetupGuide',
SettingsGeneral = 'SettingsGeneral',
SettingsDns = 'SettingsDns',
SettingsEncryption = 'SettingsEncryption',
SettingsClients = 'SettingsClients',
SettingsDhcp = 'SettingsDhcp',
Login = 'Login',
ForgotPassword = 'ForgotPassword',
}
export const Paths: Record<RoutePath, string> = {
Dashboard: pathBuilder('dashboard'),
FiltersBlocklist: pathBuilder('filters/blocklists'),
FiltersAllowlist: pathBuilder('filters/allowlists'),
FiltersRewrites: pathBuilder('filters/rewrites'),
FiltersServices: pathBuilder('filters/services'),
FiltersCustom: pathBuilder('filters/custom'),
QueryLog: pathBuilder('logs'),
SetupGuide: pathBuilder('guide'),
SettingsGeneral: pathBuilder('settings/general'),
SettingsDns: pathBuilder('settings/dns'),
SettingsEncryption: pathBuilder('settings/encryption'),
SettingsClients: pathBuilder('settings/clients'),
SettingsDhcp: pathBuilder('settings/dhcp'),
Login: pathBuilder(''),
ForgotPassword: pathBuilder('forgot_password'),
};
export enum LinkParamsKeys {}
export enum QueryParams {}
export type LinkParams = Partial<Record<LinkParamsKeys, string | number>>;
export const linkPathBuilder = (
route: RoutePath,
params?: LinkParams,
lang?: Locale,
query?: Partial<Record<QueryParams, string | number | boolean>>,
) => {
let path = Paths[route]; // .replace(BasicPath, `/${lang}`);
if (params) {
Object.keys(params).forEach((key: unknown) => {
path = path.replace(`:${key}`, String(params[key as LinkParamsKeys]));
});
}
if (query) {
path += `?${qs.stringify(query)}`;
}
return path;
};

View File

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

View File

@@ -0,0 +1,76 @@
import React, { FC, useContext } from 'react';
import { Layout } from 'antd';
import { Switch, Route, Redirect } from 'react-router-dom';
import { observer } from 'mobx-react-lite';
import Store from 'Store';
import { Paths } from './Paths';
import Dashboard from '../Dashboard';
import { Login, ForgotPassword } from '../Login';
import Sidebar from '../Sidebar';
import Header from '../Header';
import SetupGuide from '../SetupGuide';
import { GeneralSettings } from '../Settings';
import s from './Routes.module.pcss';
const { Content } = Layout;
const AuthRoutes: FC = React.memo(() => {
return (
<Switch>
<Route
exact
path={Paths.ForgotPassword}
component={ForgotPassword}
/>
<Route
path={Paths.Login}
component={Login}
/>
</Switch>
);
});
const AppRoutes: FC = observer(() => {
return (
<Layout className={s.app}>
<Sidebar />
<Layout>
<Header />
<Content>
<Switch>
<Route
exact
path={Paths.Dashboard}
component={Dashboard}
/>
<Route
exact
path={Paths.SetupGuide}
component={SetupGuide}
/>
<Route
exact
path={Paths.SettingsGeneral}
component={GeneralSettings}
/>
<Redirect to={Paths.Dashboard} />
</Switch>
</Content>
</Layout>
</Layout>
);
});
const Routes: FC = observer(() => {
const store = useContext(Store);
const { login: { loggedIn } } = store;
if (loggedIn) {
return <AppRoutes />;
}
return <AuthRoutes />;
});
export default Routes;

View File

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

View File

@@ -0,0 +1,52 @@
import React, { FC, useContext, useEffect } from 'react';
import { Tabs, Grid } from 'antd';
import { observer } from 'mobx-react-lite';
import { InnerLayout } from 'Common/ui';
import Store from 'Store';
import { General, QueryLog, Statistics, TAB_KEY } from './components';
const { useBreakpoint } = Grid;
const { TabPane } = Tabs;
const GeneralSettings: FC = observer(() => {
const store = useContext(Store);
const { ui: { intl }, generalSettings } = store;
const { inited } = generalSettings;
const screens = useBreakpoint();
useEffect(() => {
if (!inited) {
generalSettings.init();
}
}, [inited]);
if (!inited) {
return null;
}
const tabsPosition = screens.lg ? 'left' : 'top';
return (
<InnerLayout title={intl.getMessage('general_settings')}>
<Tabs
defaultActiveKey={TAB_KEY.GENERAL}
tabPosition={tabsPosition}
className="tabs"
>
<TabPane tab={intl.getMessage('filter_category_general')} key={TAB_KEY.GENERAL}>
<General/>
</TabPane>
<TabPane tab={intl.getMessage('query_log_configuration')} key={TAB_KEY.QUERY_LOG}>
<QueryLog/>
</TabPane>
<TabPane tab={intl.getMessage('statistics_configuration')} key={TAB_KEY.STATISTICS}>
<Statistics/>
</TabPane>
</Tabs>
</InnerLayout>
);
});
export default GeneralSettings;

View File

@@ -0,0 +1,45 @@
.title {
font-size: 20px;
font-weight: 500;
color: var(--gray900);
margin-bottom: 48px;
display: flex;
justify-content: space-between;
}
.radio {
display: block;
height: 30px;
line-height: 30px;
&:first-of-type {
margin-top: -12px;
}
}
.save {
display: block;
margin-top: 24px;
}
.item {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
}
.nameTitle {
color: var(--black);
}
.nameDesc {
color: var(--gray700);
margin-right: 40px;
@media (--m-viewport) {
margin-right: 200px;
}
}
.select {
margin-bottom: 24px;
margin-top: -12px;
width: 200px;
}

View File

@@ -0,0 +1,169 @@
import React, { FC, useContext } from 'react';
import { Button, Switch, Select } from 'antd';
import { Formik, FormikHelpers } from 'formik';
import { observer } from 'mobx-react-lite';
import { Link } from 'Common/ui';
import Store from 'Store';
import { RoutePath } from 'Paths';
import { s } from '.';
const { Option } = Select;
const General: FC = observer(() => {
const store = useContext(Store);
const { ui: { intl }, generalSettings } = store;
const {
safebrowsing,
filteringConfig,
parental,
safesearch,
} = generalSettings;
const initialValues = {
...filteringConfig!.serialize(),
safebrowsing,
parental,
safesearch,
};
type InitialValues = typeof initialValues;
const onSubmit = async (values: InitialValues, helpers: FormikHelpers<InitialValues>) => {
// await generalSettings.updateQueryLogConfig(values);
if (initialValues.parental !== values.parental) {
generalSettings[values.parental ? 'parentalEnable' : 'parentalDisable']();
}
if (initialValues.safesearch !== values.safesearch) {
generalSettings[values.safesearch ? 'safebrowsingEnable' : 'safebrowsingDisable']();
}
if (initialValues.safebrowsing !== values.safebrowsing) {
generalSettings[values.safebrowsing ? 'safebrowsingEnable' : 'safebrowsingDisable']();
}
if (initialValues.enabled !== values.enabled
|| initialValues.interval !== values.interval) {
generalSettings.updateFilteringConfig({
interval: values.interval,
enabled: values.enabled,
});
}
helpers.setSubmitting(false);
};
const filtersLink = (e: string) => {
// TODO: fix link
return <Link to={RoutePath.Dashboard}>{e}</Link>;
};
return (
<>
<div className={s.title}>
{intl.getMessage('filter_category_general')}
</div>
<Formik
enableReinitialize
initialValues={initialValues}
onSubmit={onSubmit}
>
{({
handleSubmit,
values,
setFieldValue,
isSubmitting,
dirty,
}) => (
<form onSubmit={handleSubmit} noValidate className={s.form}>
<div className={s.item}>
<div>
<div className={s.nameTitle}>
{intl.getMessage('block_domain_use_filters_and_hosts')}
</div>
<div className={s.nameDesc}>
{intl.getMessage('filters_block_toggle_hint', { a: filtersLink })}
</div>
</div>
<Switch checked={values.enabled} onChange={(e) => setFieldValue('enabled', e)}/>
</div>
<div className={s.item}>
<div>
<div className={s.nameTitle}>
{intl.getMessage('filters_interval')}
</div>
</div>
</div>
<Select
value={values.interval}
onChange={(e) => setFieldValue('interval', e)}
className={s.select}
>
<Option value={0}>
{intl.getMessage('disabled')}
</Option>
<Option value={1}>
{intl.getPlural('interval_hours', 1, { count: 1 })}
</Option>
<Option value={12}>
{intl.getPlural('interval_hours', 12, { count: 12 })}
</Option>
<Option value={24}>
{intl.getPlural('interval_hours', 24, { count: 24 })}
</Option>
<Option value={72}>
{intl.getPlural('interval_days', 3, { count: 3 })}
</Option>
<Option value={168}>
{intl.getPlural('interval_days', 7, { count: 7 })}
</Option>
</Select>
<div className={s.item}>
<div>
<div className={s.nameTitle}>
{intl.getMessage('use_adguard_browsing_sec')}
</div>
<div className={s.nameDesc}>
{intl.getMessage('use_adguard_browsing_sec_hint')}
</div>
</div>
<Switch checked={values.safebrowsing} onChange={(e) => setFieldValue('safebrowsing', e)}/>
</div>
<div className={s.item}>
<div>
<div className={s.nameTitle}>
{intl.getMessage('use_adguard_parental')}
</div>
<div className={s.nameDesc}>
{intl.getMessage('use_adguard_parental_hint')}
</div>
</div>
<Switch checked={values.parental} onChange={(e) => setFieldValue('parental', e)}/>
</div>
<div className={s.item}>
<div>
<div className={s.nameTitle}>
{intl.getMessage('enforce_safe_search')}
</div>
<div className={s.nameDesc}>
{intl.getMessage('enforce_save_search_hint')}
</div>
</div>
<Switch checked={values.safesearch} onChange={(e) => setFieldValue('safesearch', e)}/>
</div>
{dirty && (
<Button
type="primary"
htmlType="submit"
className={s.save}
disabled={isSubmitting}
>
{intl.getMessage('save_btn')}
</Button>
)}
</form>
)}
</Formik>
</>
);
});
export default General;

View File

@@ -0,0 +1,124 @@
import React, { FC, useContext, useState } from 'react';
import { Radio, Button, Switch } from 'antd';
import { Formik, FormikHelpers } from 'formik';
import { observer } from 'mobx-react-lite';
import { notifySuccess, ConfirmModalLayout } from 'Common/ui';
import { IQueryLogConfig } from 'Entities/QueryLogConfig';
import Store from 'Store';
import { s } from '.';
const { Group } = Radio;
const QueryLog: FC = observer(() => {
const store = useContext(Store);
const [showConfirm, setShowConfirm] = useState(false);
const { ui: { intl }, generalSettings } = store;
const {
queryLogConfig,
} = generalSettings;
const onSubmit = async (values: IQueryLogConfig, helpers: FormikHelpers<IQueryLogConfig>) => {
await generalSettings.updateQueryLogConfig(values);
helpers.setSubmitting(false);
};
const onReset = async () => {
const result = await generalSettings.querylogClear();
if (result) {
notifySuccess(intl.getMessage('query_log_cleared'));
}
};
return (
<>
<div className={s.title}>
{intl.getMessage('query_log_configuration')}
<Button onClick={() => setShowConfirm(true)}>
{intl.getMessage('query_log_clear')}
</Button>
</div>
<ConfirmModalLayout
visible={showConfirm}
onConfirm={onReset}
onClose={() => setShowConfirm(false)}
title={intl.getMessage('query_log_clear')}
buttonText={intl.getMessage('query_log_clear')}
>
{intl.getMessage('query_log_confirm_clear')}
</ConfirmModalLayout>
<Formik
enableReinitialize
initialValues={queryLogConfig!.serialize()}
onSubmit={onSubmit}
>
{({
handleSubmit,
values,
setFieldValue,
isSubmitting,
dirty,
}) => (
<form onSubmit={handleSubmit} noValidate className={s.form}>
<div className={s.item}>
<div>
<div className={s.nameTitle}>
{intl.getMessage('query_log_enable')}
</div>
</div>
<Switch checked={values.enabled} onChange={(e) => setFieldValue('enabled', e)}/>
</div>
<div className={s.item}>
<div>
<div className={s.nameTitle}>
{intl.getMessage('anonymize_client_ip')}
</div>
<div className={s.nameDesc}>
{intl.getMessage('anonymize_client_ip_desc')}
</div>
</div>
<Switch checked={values.anonymize_client_ip} onChange={(e) => setFieldValue('anonymize_client_ip', e)}/>
</div>
<div className={s.item}>
<div>
<div className={s.nameTitle}>
{intl.getMessage('query_log_retention')}
</div>
<div className={s.nameDesc}>
{intl.getMessage('query_log_retention_confirm')}
</div>
</div>
</div>
<Group value={values.interval} onChange={(e) => setFieldValue('interval', e.target.value)}>
<Radio value={1} className={s.radio}>
{intl.getMessage('interval_24_hour')}
</Radio>
<Radio value={7} className={s.radio}>
{intl.getPlural('interval_days', 7, { count: 7 })}
</Radio>
<Radio value={30} className={s.radio}>
{intl.getPlural('interval_days', 30, { count: 30 })}
</Radio>
<Radio value={90} className={s.radio}>
{intl.getPlural('interval_days', 90, { count: 90 })}
</Radio>
</Group>
{dirty && (
<Button
type="primary"
htmlType="submit"
className={s.save}
disabled={isSubmitting}
>
{intl.getMessage('save_btn')}
</Button>
)}
</form>
)}
</Formik>
</>
);
});
export default QueryLog;

View File

@@ -0,0 +1,105 @@
import React, { FC, useContext, useState } from 'react';
import { Radio, Button } from 'antd';
import { Formik, FormikHelpers } from 'formik';
import { observer } from 'mobx-react-lite';
import { notifySuccess, ConfirmModalLayout } from 'Common/ui';
import { IStatsConfig } from 'Entities/StatsConfig';
import Store from 'Store';
import { s } from '.';
const { Group } = Radio;
const Statistics: FC = observer(() => {
const store = useContext(Store);
const [showConfirm, setShowConfirm] = useState(false);
const { ui: { intl }, generalSettings } = store;
const {
statsConfig,
} = generalSettings;
const onSubmit = async (values: IStatsConfig, helpers: FormikHelpers<IStatsConfig>) => {
await generalSettings.updateStatsConfig(values);
helpers.setSubmitting(false);
};
const onReset = async () => {
const result = await generalSettings.statsReset();
if (result) {
notifySuccess(intl.getMessage('stats_reset'));
}
};
return (
<>
<div className={s.title}>
{intl.getMessage('statistics_configuration')}
<Button onClick={() => setShowConfirm(true)}>
{intl.getMessage('statistics_clear')}
</Button>
</div>
<ConfirmModalLayout
visible={showConfirm}
onConfirm={onReset}
onClose={() => setShowConfirm(false)}
title={intl.getMessage('statistics_clear')}
buttonText={intl.getMessage('statistics_clear')}
>
{intl.getMessage('statistics_clear_confirm')}
</ConfirmModalLayout>
<Formik
enableReinitialize
initialValues={statsConfig!.serialize()}
onSubmit={onSubmit}
>
{({
handleSubmit,
values,
setFieldValue,
isSubmitting,
dirty,
}) => (
<form onSubmit={handleSubmit} noValidate>
<div className={s.item}>
<div>
<div className={s.nameTitle}>
{intl.getMessage('statistics_retention')}
</div>
<div className={s.nameDesc}>
{intl.getMessage('statistics_retention_desc')}
</div>
</div>
</div>
<Group value={values.interval} onChange={(e) => setFieldValue('interval', e.target.value)}>
<Radio value={1} className={s.radio}>
{intl.getMessage('interval_24_hour')}
</Radio>
<Radio value={7} className={s.radio}>
{intl.getPlural('interval_days', 7, { count: 7 })}
</Radio>
<Radio value={30} className={s.radio}>
{intl.getPlural('interval_days', 30, { count: 30 })}
</Radio>
<Radio value={90} className={s.radio}>
{intl.getPlural('interval_days', 90, { count: 90 })}
</Radio>
</Group>
{dirty && (
<Button
type="primary"
htmlType="submit"
className={s.save}
disabled={isSubmitting}
>
{intl.getMessage('save_btn')}
</Button>
)}
</form>
)}
</Formik>
</>
);
});
export default Statistics;

View File

@@ -0,0 +1,9 @@
export { default as General } from './General';
export { default as QueryLog } from './QueryLog';
export { default as Statistics } from './Statistics';
export enum TAB_KEY {
GENERAL = 'GENERAL',
QUERY_LOG = 'QUERY_LOG',
STATISTICS = 'STATISTICS',
}
export { default as s } from './Common.module.pcss';

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
.title {
margin-bottom: 16px;
font-size: 20px;
font-weight: 500;
@media (--m-viewport) {
margin-bottom: 24px;
}
}
.text {
margin-bottom: 32px;
font-size: 14px;
color: var(--gray900);
p {
margin: 0 0 5px;
}
}
.addresses {
margin-top: 16px;
}
.address {
font-family: var(--font-family-monospace);
font-size: 16px;
font-weight: 600;
word-break: break-all;
color: var(--green400);
}

View File

@@ -0,0 +1,92 @@
import React, { FC, useContext } from 'react';
import { Tabs, Grid } from 'antd';
import { InnerLayout } from 'Common/ui';
import { externalLink, p } from 'Common/formating';
import { DHCP_LINK } from 'Consts/common';
import Store from 'Store';
import s from './SetupGuide.module.pcss';
const { useBreakpoint } = Grid;
const { TabPane } = Tabs;
const SetupGuide: FC = () => {
const store = useContext(Store);
const { ui: { intl }, system } = store;
const screens = useBreakpoint();
const tabsPosition = screens.lg ? 'left' : 'top';
const { status } = system;
const tabs = [
{
key: intl.getMessage('router'),
text: intl.getMessage('install_configure_router', { p }),
},
{
key: 'Windows',
text: intl.getMessage('install_configure_windows', { p }),
},
{
key: 'macOS',
text: intl.getMessage('install_configure_macos', { p }),
},
{
key: 'Linux',
text: intl.getMessage('install_configure_router', { p }),
},
{
key: 'Android',
text: intl.getMessage('install_configure_android', { p }),
},
{
key: 'iOS',
text: intl.getMessage('install_configure_ios', { p }),
},
];
const addresses = (
<>
<div className={s.text}>
{intl.getMessage('install_configure_adresses')}
{status?.dnsAddresses && (
<div className={s.addresses}>
{status.dnsAddresses.map((address) => (
<div className={s.address} key={address}>
{address}
</div>
))}
</div>
)}
</div>
<div className={s.text}>
{intl.getMessage('install_configure_dhcp', { dhcp: externalLink(DHCP_LINK) })}
</div>
</>
);
return (
<InnerLayout title={intl.getMessage('setup_guide')}>
<Tabs
defaultActiveKey={intl.getMessage('router')}
tabPosition={tabsPosition}
className="tabs"
>
{tabs.map((tab) => (
<TabPane tab={tab.key} key={tab.key}>
<div className={s.title}>
{intl.getMessage('install_configure_how_to_title', { value: tab.key })}
</div>
<div className={s.text}>
{tab.text}
</div>
{addresses}
</TabPane>
))}
</Tabs>
</InnerLayout>
);
};
export default SetupGuide;

View File

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

View File

@@ -0,0 +1,23 @@
.logo {
width: 118px;
height: 31px;
margin: 20px;
}
.icon {
width: 16px;
height: 16px;
margin-right: 10px;
}
.menu {
display: flex;
flex-direction: column;
min-height: calc(100% - 71px);
}
.logout {
@media (--m-viewport) {
margin-top: auto!important;
}
}

View File

@@ -0,0 +1,116 @@
import React, { FC, useContext } from 'react';
import { Layout, Menu, Grid } from 'antd';
import { observer } from 'mobx-react-lite';
import { PieChartOutlined, FormOutlined, TableOutlined, ProfileOutlined, SettingOutlined } from '@ant-design/icons';
import Store from 'Store';
import { Link, Icon, Mask } from 'Common/ui';
import { RoutePath, linkPathBuilder } from 'Components/App/Routes/Paths';
import s from './Sidebar.module.pcss';
const { Sider } = Layout;
const { Item: MenuItem, SubMenu } = Menu;
const { useBreakpoint } = Grid;
const Sidebar: FC = observer(() => {
const store = useContext(Store);
const screens = useBreakpoint();
const { ui: { intl, sidebarOpen, toggleSidebar } } = store;
if (!Object.keys(screens).length) {
return null;
}
const handleSidebar = () => {
if (!screens.xl) {
toggleSidebar();
}
};
return (
<>
<Sider
collapsed={!sidebarOpen && !screens.xl}
collapsedWidth={0}
collapsible
onClick={handleSidebar}
className="sidebar"
trigger={null}
width={200}
>
<Icon icon="logo_light" className={s.logo} />
<Menu
mode="inline"
theme="dark"
className={s.menu}
>
<MenuItem key={linkPathBuilder(RoutePath.Dashboard)}>
<Link to={RoutePath.Dashboard}>
<PieChartOutlined className={s.icon} />
{intl.getMessage('dashboard')}
</Link>
</MenuItem>
<MenuItem key={linkPathBuilder(RoutePath.FiltersBlocklist)}>
<Link to={RoutePath.FiltersBlocklist}>
<FormOutlined className={s.icon} />
{intl.getMessage('filters')}
</Link>
</MenuItem>
<MenuItem key={linkPathBuilder(RoutePath.QueryLog)}>
<Link to={RoutePath.QueryLog}>
<TableOutlined className={s.icon} />
{intl.getMessage('query_log')}
</Link>
</MenuItem>
<MenuItem key={linkPathBuilder(RoutePath.SetupGuide)}>
<Link to={RoutePath.SetupGuide}>
<ProfileOutlined className={s.icon} />
{intl.getMessage('setup_guide')}
</Link>
</MenuItem>
<SubMenu
key="settings"
icon={<SettingOutlined className={s.icon} />}
title={intl.getMessage('settings')}
>
<Menu.Item key={linkPathBuilder(RoutePath.SettingsGeneral)}>
<Link to={RoutePath.SettingsGeneral}>
{intl.getMessage('general_settings')}
</Link>
</Menu.Item>
<Menu.Item key={linkPathBuilder(RoutePath.SettingsDns)}>
<Link to={RoutePath.SettingsDns}>
{intl.getMessage('dns_settings')}
</Link>
</Menu.Item>
<Menu.Item key={linkPathBuilder(RoutePath.SettingsEncryption)}>
<Link to={RoutePath.SettingsEncryption}>
{intl.getMessage('encryption_settings')}
</Link>
</Menu.Item>
<Menu.Item key={linkPathBuilder(RoutePath.SettingsClients)}>
<Link to={RoutePath.SettingsClients}>
{intl.getMessage('client_settings')}
</Link>
</Menu.Item>
<Menu.Item key={linkPathBuilder(RoutePath.SettingsDhcp)}>
<Link to={RoutePath.SettingsDhcp}>
{intl.getMessage('dhcp_settings')}
</Link>
</Menu.Item>
</SubMenu>
<MenuItem className={s.logout}>
<a href="control/logout">
<Icon icon="sign_out" className={s.icon} />
{intl.getMessage('sign_out')}
</a>
</MenuItem>
</Menu>
</Sider>
<Mask open={sidebarOpen} handle={handleSidebar} />
</>
);
});
export default Sidebar;

View File

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

View File

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

View File

@@ -2,9 +2,10 @@ import React, { FC } from 'react';
import { Layout } from 'antd';
import { Formik, FormikHelpers } from 'formik';
import { observer } from 'mobx-react-lite';
import cn from 'classnames';
import { IInitialConfigurationBeta } from 'Entities/InitialConfigurationBeta';
import Icons from 'Lib/theme/Icons';
import Icons from 'Common/ui/Icons';
import {
DEFAULT_DNS_ADDRESS,
DEFAULT_DNS_PORT,
@@ -109,8 +110,8 @@ const InstallForm: FC = observer(() => {
const Install: FC = () => {
return (
<Layout className={theme.install.layout}>
<Content className={theme.install.container}>
<Layout className={cn(theme.content.content, theme.content.content_auth)}>
<Content className={cn(theme.content.container, theme.content.container_auth)}>
<InstallForm />
</Content>
<Icons/>

View File

@@ -3,10 +3,11 @@ import { Tabs, Grid } from 'antd';
import cn from 'classnames';
import { FormikHelpers } from 'formik';
import { DHCP_LINK } from 'Consts/common';
import { danger, externalLink, p } from 'Common/formating';
import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/install';
import Store from 'Store/installStore';
import theme from 'Lib/theme';
import { danger, p } from 'Common/formating';
import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/install';
import { FormValues } from '../../Install';
import StepButtons from '../StepButtons';
@@ -26,17 +27,6 @@ const ConfigureDevices: FC<ConfigureDevicesProps> = ({
const screens = useBreakpoint();
const tabsPosition = screens.md ? 'left' : 'top';
const dhcp = (e: string) => (
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP"
target="_blank"
rel="noopener noreferrer"
className={theme.link.link}
>
{e}
</a>
);
const allIps = addresses?.interfaces.reduce<string[]>((all, data) => {
const { ipAddresses } = data;
if (ipAddresses) {
@@ -138,7 +128,7 @@ const ConfigureDevices: FC<ConfigureDevicesProps> = ({
</div>
</div>
<div className={cn(theme.install.text, theme.install.text_base)}>
{intl.getMessage('install_configure_dhcp', { dhcp })}
{intl.getMessage('install_configure_dhcp', { dhcp: externalLink(DHCP_LINK) })}
</div>
<StepButtons
setFieldValue={setFieldValue}

View File

@@ -0,0 +1,67 @@
import React, { FC, FocusEvent } from 'react';
import { Button as ButtonControl } from 'antd';
import cn from 'classnames';
type ButtonSize = 'small' | 'medium' | 'big';
type ButtonType = 'primary' | 'icon' | 'link' | 'outlined' | 'border' | 'ghost' | 'input' | 'edit';
type ButtonHTMLType = 'submit' | 'button' | 'reset';
type ButtonShape = 'circle' | 'round';
export interface ButtonProps {
className?: string;
danger?: boolean;
dataAttrs?: {
[key: string]: string;
};
disabled?: boolean;
htmlType?: ButtonHTMLType;
// icon?: IconType | 'dots_loader';
iconClassName?: string;
id?: string;
inGroup?: boolean;
onClick?: React.MouseEventHandler<HTMLElement>;
onBlur?: (e: FocusEvent<HTMLInputElement>) => void;
shape?: ButtonShape;
size?: ButtonSize;
type: ButtonType;
block?: boolean;
}
const Button: FC<ButtonProps> = ({
children,
className,
danger,
dataAttrs,
disabled,
htmlType,
// icon,
id,
onClick,
onBlur,
shape,
}) => {
const buttonClass = cn(
className,
);
return (
<ButtonControl
className={buttonClass}
danger={danger}
disabled={disabled}
{...dataAttrs}
htmlType={htmlType}
// icon={icon && (icon === 'dots_loader'
// ? <Dots className={iconClassName} />
// : <Icon icon={icon} className={iconClassName} />)}
id={id}
onClick={onClick}
onBlur={onBlur}
shape={shape}
>
{children}
</ButtonControl>
);
};
export default Button;

View File

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

View File

@@ -6,7 +6,7 @@ import s from './Radio.module.pcss';
const { Group } = Radio;
interface AdminInterfaceProps {
interface RadioProps {
options: {
label: string;
desc?: string;
@@ -16,7 +16,7 @@ interface AdminInterfaceProps {
value: string | number;
}
const RadioComponent: FC<AdminInterfaceProps> = observer(({
const RadioComponent: FC<RadioProps> = observer(({
options, onSelect, value,
}) => {
if (options.length === 0) {

View File

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

View File

@@ -0,0 +1,12 @@
import React from 'react';
import theme from 'Lib/theme';
const code = (e: string) => {
return (
<code className={theme.text.code}>
{e}
</code>
);
};
export default code;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import theme from 'Lib/theme';
export const externalLink = (link: string) => (e: string) => (
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className={theme.link.link}
>
{e}
</a>
);

View File

@@ -1,2 +1,4 @@
export { default as danger } from './danger';
export { default as p } from './p';
export { default as code } from './code';
export { externalLink } from './externalLink';

View File

@@ -1,6 +1,6 @@
import React, { FC } from 'react';
import cn from 'classnames';
import { IconType } from 'Lib/theme/Icons';
import { IconType } from 'Common/ui/Icons';
import s from './Icon.module.pcss';
@@ -22,4 +22,4 @@ const Icon: FC<IconProps> = ({ icon, color, className, onClick }) => {
};
export default Icon;
export { IconType } from 'Lib/theme/Icons';
export { IconType } from 'Common/ui/Icons';

View File

@@ -0,0 +1,3 @@
.icons {
display: none;
}

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

View File

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

View File

@@ -0,0 +1,23 @@
import React, { FC, useContext } from 'react';
import { Icon } from 'Common/ui';
import Store from 'Store';
import { LANGUAGES } from 'Localization';
import s from './LangSelect.module.pcss';
const LangSelector: FC = () => {
const store = useContext(Store);
const { ui: { currentLang } } = store;
const lang = LANGUAGES.find((e) => e.code === currentLang)!;
return (
<div className={s.wrap}>
<Icon icon="language" className={s.icon} />
{lang.name}
</div>
);
};
export default LangSelector;

View File

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

View File

@@ -0,0 +1,63 @@
import React, { FC, MouseEvent } from 'react';
import { Link as L, LinkProps as LProps } from 'react-router-dom';
import cn from 'classnames';
import { linkPathBuilder, RoutePath, LinkParams, LinkParamsKeys } from 'Paths';
interface LinkProps {
to: RoutePath;
props?: LinkParams;
className?: string;
type?: LProps['type'];
stop?: boolean;
disabled?: boolean;
onClick?: () => void;
id?: string;
}
const Link: FC<LinkProps> = ({
to, children, className, props, type, stop, disabled, onClick, id,
}) => {
if (props) {
Object.keys(props).forEach((key: unknown) => {
if (!props[key as LinkParamsKeys]) {
throw new Error(`Got wrong ${key} propKey: ${props[key as LinkParamsKeys]} in Link`);
}
});
}
const handleClick = (e: MouseEvent) => {
if (stop) {
e.stopPropagation();
}
if (onClick) {
onClick();
}
};
if (disabled) {
return (
<div
id={id}
tabIndex={0}
className={cn(className)}
>
{children}
</div>
);
}
return (
<L
id={id}
className={className}
type={type}
to={linkPathBuilder(to, props)}
onClick={handleClick}
>
{children}
</L>
);
};
export default Link;

View File

@@ -0,0 +1,26 @@
.mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1040;
height: 100%;
background-color: rgba(0, 0, 0, 0.45);
opacity: 0;
visibility: hidden;
transition: opacity var(--transition);
cursor: pointer;
&_visible {
opacity: 1;
visibility: visible;
}
@media (--l-viewport) {
&_visible {
opacity: 0;
visibility: hidden;
}
}
}

View File

@@ -0,0 +1,23 @@
import React, { FC } from 'react';
import cn from 'classnames';
import s from './Mask.module.pcss';
interface MaskProps {
open: boolean;
handle: () => void;
}
const Mask: FC<MaskProps> = ({ open, handle }) => {
return (
<div
className={cn(
s.mask,
{ [s.mask_visible]: open },
)}
onClick={handle}
/>
);
};
export default Mask;

View File

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

View File

@@ -1,2 +1,6 @@
export { default as Icon } from './Icon';
export { notifyError, notifySuccess } from './Notifications';
export { default as Link } from './Link';
export { default as LangSelect } from './LangSelect';
export { default as Mask } from './Mask';
export { CommonLayout, InnerLayout, CommonModalLayout, ConfirmModalLayout } from './layouts';

View File

@@ -0,0 +1,16 @@
import { Layout } from 'antd';
import React, { FC } from 'react';
interface CommonLayoutProps {
className?: string;
}
const CommonLayout: FC<CommonLayoutProps> = ({ children, className }) => {
return (
<Layout className={className}>
{children}
</Layout>
);
};
export default CommonLayout;

View File

@@ -0,0 +1,87 @@
import React, { FC, useContext, useEffect } from 'react';
import { Modal, Button } from 'antd';
import cn from 'classnames';
import { Icon } from 'Common/ui';
import Store from 'Store';
interface CommonModalLayoutProps {
visible: boolean;
title: string;
buttonText?: string;
className?: string;
width?: number;
onClose: () => void;
onSubmit?: () => void;
noFooter?: boolean;
disabled?: boolean;
centered?: boolean;
}
const CommonModalLayout: FC<CommonModalLayoutProps> = ({
visible,
children,
title,
buttonText,
className,
width,
onClose,
onSubmit,
noFooter,
disabled,
centered,
}) => {
const store = useContext(Store);
const { ui: { intl } } = store;
useEffect(() => {
const onEnter = (e: KeyboardEvent) => {
if (e.key === 'Enter' && onSubmit) {
onSubmit();
}
};
if (onSubmit) {
window.addEventListener('keyup', onEnter);
}
return () => {
window.removeEventListener('keyup', onEnter);
};
}, [onSubmit]);
const footer = noFooter ? null : [
<Button
type="primary"
size="large"
key="submit"
htmlType="submit"
onClick={onSubmit}
disabled={disabled}
>
{buttonText}
</Button>,
<Button
type="link"
size="large"
key="cancel"
onClick={onClose}
>
{intl.getMessage('cancel')}
</Button>,
];
return (
<Modal
visible={visible}
title={title}
wrapClassName={cn('modal', className)}
onCancel={onClose}
footer={footer}
closeIcon={<Icon icon="close_big" />}
width={width || 480}
centered={centered}
>
{children}
</Modal>
);
};
export default CommonModalLayout;

View File

@@ -0,0 +1,34 @@
import React, { FC } from 'react';
import CommonModalLayout from './CommonModalLayout';
interface DeleteModalLayoutProps {
visible: boolean;
title: string;
buttonText: string;
onClose: () => void;
onConfirm?: () => void;
}
const DeleteModalLayout: FC<DeleteModalLayoutProps> = ({
visible,
children,
title,
buttonText,
onClose,
onConfirm,
}) => {
return (
<CommonModalLayout
visible={visible}
title={title}
buttonText={buttonText}
onSubmit={onConfirm}
onClose={onClose}
>
{children}
</CommonModalLayout>
);
};
export default DeleteModalLayout;

View File

@@ -0,0 +1,41 @@
import { Layout } from 'antd';
import React, { FC } from 'react';
import cn from 'classnames';
import theme from 'Lib/theme';
interface InnerLayoutProps {
title: string;
className?: string;
containerClassName?: string;
}
const InnerLayout: FC<InnerLayoutProps> = ({
children, title, className, containerClassName,
}) => {
return (
<Layout
className={cn(
theme.content.content,
theme.content.content_inner,
className,
)}
>
<div
className={cn(
theme.content.container,
containerClassName,
)}
>
<div className={theme.content.header}>
<div className={theme.content.title}>
{title}
</div>
</div>
{children}
</div>
</Layout>
);
};
export default InnerLayout;

View File

@@ -0,0 +1,4 @@
export { default as CommonLayout } from './CommonLayout';
export { default as InnerLayout } from './InnerLayout';
export { default as ConfirmModalLayout } from './ConfirmModalLayout';
export { default as CommonModalLayout } from './CommonModalLayout';