Pull request #2231: ADG-8368 Frontend rewritten in TypeScript, added Node 18 support

Merge in DNS/adguard-home from ADG-8368-typescript-node-18 to master

Squashed commit of the following:

commit daa288ae0d76178af24595cc807055902e6f09ab
Merge: 4c89cf720 1085d59a6
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Jun 10 17:22:20 2024 +0200

    merge

commit 4c89cf7209
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Jun 6 13:27:18 2024 +0300

    remove install from initial state

commit b943f2011f
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 23:10:55 2024 +0200

    frontend production build fix

commit cd1be2d66d
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 20:23:14 2024 +0200

    production build quickfix

commit 7b8ac01fc2
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 5 19:57:31 2024 +0300

    all: upd node docker

commit 02afed66d5
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 18:23:12 2024 +0200

    changelog fixes

commit 9c0f736f0c
Merge: 62c4fbf1e e04775c4f
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 18:18:29 2024 +0200

    merge

commit 62c4fbf1e3
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:22:22 2024 +0200

    empty line in changelog

commit 76b1e44a93
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:20:37 2024 +0200

    changelog

commit f783e90040
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:19:13 2024 +0200

    filters.js -> filters.ts

commit 3d4ce6554c
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:18:03 2024 +0200

    generated file removed

commit e35ba58f2a
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 15:45:21 2024 +0200

    rollback unwanted changes

commit 1f30d4216d
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 15:27:36 2024 +0200

    review fix

commit 6cd4e44f07
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 11:55:39 2024 +0200

    missing generated file restoresd

commit 2ab738b303
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 11:40:32 2024 +0200

    Frontend rewritten in TypeScript, added Node 18 support
This commit is contained in:
Igor Lobanov
2024-06-10 18:42:23 +03:00
parent 1085d59a65
commit 1afe226ce8
296 changed files with 32122 additions and 32651 deletions

View File

@@ -1,25 +1,37 @@
import React from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next';
import { TFunction } from 'i18next';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
import DomainCell from './DomainCell';
import { getPercent } from '../../helpers/helpers';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
const CountCell = (totalBlocked) => function cell(row) {
const { value } = row;
const percent = getPercent(totalBlocked, value);
const CountCell = (totalBlocked: any) =>
function cell(row: any) {
const { value } = row;
const percent = getPercent(totalBlocked, value);
return <Cell value={value}
percent={percent}
color={STATUS_COLORS.red}
search={row.original.domain}
/>;
};
return <Cell value={value} percent={percent} color={STATUS_COLORS.red} search={row.original.domain} />;
};
interface BlockedDomainsProps {
topBlockedDomains: unknown[];
blockedFiltering: number;
replacedSafebrowsing: number;
replacedSafesearch: number;
replacedParental: number;
refreshButton: React.ReactNode;
subtitle: string;
t: TFunction;
}
const BlockedDomains = ({
t,
@@ -30,20 +42,13 @@ const BlockedDomains = ({
replacedSafebrowsing,
replacedParental,
replacedSafesearch,
}) => {
const totalBlocked = (
blockedFiltering + replacedSafebrowsing + replacedParental + replacedSafesearch
);
}: BlockedDomainsProps) => {
const totalBlocked = blockedFiltering + replacedSafebrowsing + replacedParental + replacedSafesearch;
return (
<Card
title={t('top_blocked_domains')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<Card title={t('top_blocked_domains')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
<ReactTable
data={topBlockedDomains.map(({ name: domain, count }) => ({
data={topBlockedDomains.map(({ name: domain, count }: any) => ({
domain,
count,
}))}
@@ -70,15 +75,4 @@ const BlockedDomains = ({
);
};
BlockedDomains.propTypes = {
topBlockedDomains: PropTypes.array.isRequired,
blockedFiltering: PropTypes.number.isRequired,
replacedSafebrowsing: PropTypes.number.isRequired,
replacedSafesearch: PropTypes.number.isRequired,
replacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(BlockedDomains);

View File

@@ -1,10 +1,12 @@
import React, { useState } from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
@@ -16,11 +18,14 @@ import {
TABLES_MIN_ROWS,
} from '../../helpers/constants';
import { toggleClientBlock } from '../../actions/access';
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
import { getStats } from '../../actions/stats';
import IconTooltip from '../Logs/Cells/IconTooltip';
const getClientsPercentColor = (percent) => {
import IconTooltip from '../Logs/Cells/IconTooltip';
import { RootState } from '../../initialState';
const getClientsPercentColor = (percent: any) => {
if (percent > 50) {
return STATUS_COLORS.green;
}
@@ -30,9 +35,13 @@ const getClientsPercentColor = (percent) => {
return STATUS_COLORS.red;
};
const CountCell = (row) => {
const { value, original: { ip } } = row;
const numDnsQueries = useSelector((state) => state.stats.numDnsQueries, shallowEqual);
const CountCell = (row: any) => {
const {
value,
original: { ip },
} = row;
const numDnsQueries = useSelector<RootState>((state) => state.stats.numDnsQueries, shallowEqual);
const percent = getPercent(numDnsQueries, value);
const percentColor = getClientsPercentColor(percent);
@@ -40,22 +49,29 @@ const CountCell = (row) => {
return <Cell value={value} percent={percent} color={percentColor} search={ip} />;
};
const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
const renderBlockingButton = (ip: any, disallowed: any, disallowed_rule: any) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const processingSet = useSelector((state) => state.access.processingSet);
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
const processingSet = useSelector<RootState, RootState['access']['processingSet']>(
(state) => state.access.processingSet,
);
const allowedClients = useSelector<RootState, RootState['access']['allowed_clients']>(
(state) => state.access.allowed_clients,
shallowEqual,
);
const [isOptionsOpened, setOptionsOpened] = useState(false);
const toggleClientStatus = async (ip, disallowed, disallowed_rule) => {
const toggleClientStatus = async (ip: any, disallowed: any, disallowed_rule: any) => {
let confirmMessage;
if (disallowed) {
confirmMessage = t('client_confirm_unblock', { ip: disallowed_rule || ip });
} else {
confirmMessage = `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`;
if (allowedСlients.length > 0) {
if (allowedClients.length > 0) {
confirmMessage = confirmMessage.concat(`\n\n${t('filter_allowlist', { disallowed_rule })}`);
}
}
@@ -73,15 +89,11 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const lastRuleInAllowlist = !disallowed && allowedСlients === disallowed_rule;
const lastRuleInAllowlist = !disallowed && allowedClients === disallowed_rule;
const disabled = processingSet || lastRuleInAllowlist;
return (
<div className="table__action">
<button
type="button"
className="btn btn-icon btn-sm px-0"
onClick={() => setOptionsOpened(true)}
>
<button type="button" className="btn btn-icon btn-sm px-0" onClick={() => setOptionsOpened(true)}>
<svg className="icon24 icon--lightgray button-action__icon">
<use xlinkHref="#bullets" />
</svg>
@@ -92,16 +104,18 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
tooltipClass="button-action--arrow-option-container"
xlinkHref="bullets"
triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
content={(
content={
<button
className={classNames('button-action--arrow-option px-4 py-1', disallowed ? 'bg--green' : 'bg--danger')}
className={classNames(
'button-action--arrow-option px-4 py-1',
disallowed ? 'bg--green' : 'bg--danger',
)}
onClick={onClick}
disabled={disabled}
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}
>
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}>
<Trans>{text}</Trans>
</button>
)}
}
placement="bottom-end"
trigger="click"
onVisibilityChange={setOptionsOpened}
@@ -113,35 +127,42 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
);
};
const ClientCell = (row) => {
const { value, original: { info, info: { disallowed, disallowed_rule } } } = row;
return <>
<div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
{renderFormattedClientCell(value, info, true)}
{renderBlockingButton(value, disallowed, disallowed_rule)}
</div>
</>;
};
const Clients = ({
refreshButton,
subtitle,
}) => {
const { t } = useTranslation();
const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
const ClientCell = (row: any) => {
const {
value,
original: {
info,
info: { disallowed, disallowed_rule },
},
} = row;
return (
<Card
title={t('top_clients')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<>
<div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
{renderFormattedClientCell(value, info, true)}
{renderBlockingButton(value, disallowed, disallowed_rule)}
</div>
</>
);
};
interface ClientsProps {
refreshButton: React.ReactNode;
subtitle: string;
}
const Clients = ({ refreshButton, subtitle }: ClientsProps) => {
const { t } = useTranslation();
const topClients = useSelector<RootState, RootState['stats']['topClients']>(
(state) => state.stats.topClients,
shallowEqual,
);
return (
<Card title={t('top_clients')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
<ReactTable
data={topClients.map(({
name: ip, count, info, blocked,
}) => ({
data={topClients.map(({ name: ip, count, info, blocked }: any) => ({
ip,
count,
info,
@@ -167,12 +188,14 @@ const Clients = ({
minRows={TABLES_MIN_ROWS}
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
className="-highlight card-table-overflow--limited clients__table"
getTrProps={(_state, rowInfo) => {
getTrProps={(_state: any, rowInfo: any) => {
if (!rowInfo) {
return {};
}
const { info: { disallowed } } = rowInfo.original;
const {
info: { disallowed },
} = rowInfo.original;
return disallowed ? { className: 'logs__row--red' } : {};
}}
@@ -181,9 +204,4 @@ const Clients = ({
);
};
Clients.propTypes = {
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
};
export default Clients;

View File

@@ -1,41 +1,52 @@
import React from 'react';
import propTypes from 'prop-types';
import { Trans, useTranslation } from 'react-i18next';
import round from 'lodash/round';
import { shallowEqual, useSelector } from 'react-redux';
import Card from '../ui/Card';
import { formatNumber, msToDays, msToHours } from '../../helpers/helpers';
import LogsSearchLink from '../ui/LogsSearchLink';
import { RESPONSE_FILTER, TIME_UNITS } from '../../helpers/constants';
import Tooltip from '../ui/Tooltip';
const Row = ({
label, count, response_status, tooltipTitle, translationComponents,
}) => {
const content = response_status
? <LogsSearchLink response_status={response_status}>{formatNumber(count)}</LogsSearchLink>
: count;
import Tooltip from '../ui/Tooltip';
import { RootState } from '../../initialState';
interface RowProps {
label: string;
count: string;
response_status?: string;
tooltipTitle: string;
translationComponents?: React.ReactElement[];
}
const Row = ({ label, count, response_status, tooltipTitle, translationComponents }: RowProps) => {
const content = response_status ? (
<LogsSearchLink response_status={response_status}>{formatNumber(count)}</LogsSearchLink>
) : (
count
);
return (
<div className="counters__row" key={label}>
<div className="counters__column">
<span className="counters__title">
<Trans components={translationComponents}>
{label}
</Trans>
<Trans components={translationComponents}>{label}</Trans>
</span>
<span className="counters__tooltip">
<Tooltip
content={tooltipTitle}
placement="top"
className="tooltip-container tooltip-custom--narrow text-center"
>
className="tooltip-container tooltip-custom--narrow text-center">
<svg className="icons icon--20 icon--lightgray ml-2">
<use xlinkHref="#question" />
</svg>
</Tooltip>
</span>
</div>
<div className="counters__column counters__column--value">
<strong>{content}</strong>
</div>
@@ -43,7 +54,12 @@ const Row = ({
);
};
const Counters = ({ refreshButton, subtitle }) => {
interface CountersProps {
refreshButton: React.ReactNode;
subtitle: string;
}
const Counters = ({ refreshButton, subtitle }: CountersProps) => {
const {
interval,
numDnsQueries,
@@ -53,77 +69,67 @@ const Counters = ({ refreshButton, subtitle }) => {
numReplacedSafesearch,
avgProcessingTime,
timeUnits,
} = useSelector((state) => state.stats, shallowEqual);
} = useSelector<RootState, RootState['stats']>((state) => state.stats, shallowEqual);
const { t } = useTranslation();
const dnsQueryTooltip = timeUnits === TIME_UNITS.HOURS
? t('number_of_dns_query_hours', { count: msToHours(interval) })
: t('number_of_dns_query_days', { count: msToDays(interval) });
const dnsQueryTooltip =
timeUnits === TIME_UNITS.HOURS
? t('number_of_dns_query_hours', { count: msToHours(interval) })
: t('number_of_dns_query_days', { count: msToDays(interval) });
const rows = [
{
label: 'dns_query',
count: numDnsQueries,
count: numDnsQueries.toString(),
tooltipTitle: dnsQueryTooltip,
response_status: RESPONSE_FILTER.ALL.QUERY,
},
{
label: 'blocked_by',
count: numBlockedFiltering,
count: numBlockedFiltering.toString(),
tooltipTitle: 'number_of_dns_query_blocked_24_hours',
response_status: RESPONSE_FILTER.BLOCKED.QUERY,
translationComponents: [<a href="#filters" key="0">link</a>],
translationComponents: [
<a href="#filters" key="0">
link
</a>,
],
},
{
label: 'stats_malware_phishing',
count: numReplacedSafebrowsing,
count: numReplacedSafebrowsing.toString(),
tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec',
response_status: RESPONSE_FILTER.BLOCKED_THREATS.QUERY,
},
{
label: 'stats_adult',
count: numReplacedParental,
count: numReplacedParental.toString(),
tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult',
response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY,
},
{
label: 'enforced_save_search',
count: numReplacedSafesearch,
count: numReplacedSafesearch.toString(),
tooltipTitle: 'number_of_dns_query_to_safe_search',
response_status: RESPONSE_FILTER.SAFE_SEARCH.QUERY,
},
{
label: 'average_processing_time',
count: avgProcessingTime ? `${round(avgProcessingTime)} ms` : 0,
count: avgProcessingTime ? `${round(avgProcessingTime)} ms` : '0',
tooltipTitle: 'average_processing_time_hint',
},
];
return (
<Card
title={t('general_statistics')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<Card title={t('general_statistics')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
<div className="counters">
{rows.map(Row)}
{rows.map((row, index) => {
return <Row {...row} key={index} />;
})}
</div>
</Card>
);
};
Row.propTypes = {
label: propTypes.string.isRequired,
count: propTypes.string.isRequired,
response_status: propTypes.string,
tooltipTitle: propTypes.string.isRequired,
translationComponents: propTypes.arrayOf(propTypes.element),
};
Counters.propTypes = {
refreshButton: propTypes.node.isRequired,
subtitle: propTypes.string.isRequired,
};
export default Counters;

View File

@@ -1,77 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
import { getSourceData, getTrackerData } from '../../helpers/trackers/trackers';
import Tooltip from '../ui/Tooltip';
import { captitalizeWords } from '../../helpers/helpers';
const renderLabel = (value) => <strong><Trans>{value}</Trans></strong>;
const renderLink = ({ url, name }) => <a
className="tooltip-custom__content-link"
target="_blank"
rel="noopener noreferrer"
href={url}
>
<strong>{name}</strong>
</a>;
const getTrackerInfo = (trackerData) => [{
key: 'name_table_header',
value: trackerData,
render: renderLink,
},
{
key: 'category_label',
value: captitalizeWords(trackerData.category),
render: renderLabel,
},
{
key: 'source_label',
value: getSourceData(trackerData),
render: renderLink,
}];
const DomainCell = ({ value }) => {
const trackerData = getTrackerData(value);
const content = trackerData && <div className="popover__list">
<div className="tooltip-custom__content-title mb-1">
<Trans>found_in_known_domain_db</Trans>
</div>
{getTrackerInfo(trackerData)
.map(({ key, value, render }) => <div
key={key}
className="tooltip-custom__content-item"
>
<Trans>{key}</Trans>: {render(value)}
</div>)}
</div>;
return (
<div className="logs__row">
<div className="logs__text" title={value}>
{value}
</div>
{trackerData
&& <Tooltip content={content} placement="top"
className="tooltip-container tooltip-custom--wide">
<svg className="icons icon--24 icon--green ml-1">
<use xlinkHref="#privacy" />
</svg>
</Tooltip>}
</div>
);
};
DomainCell.propTypes = {
value: PropTypes.string.isRequired,
};
renderLink.propTypes = {
url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
};
export default DomainCell;

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Trans } from 'react-i18next';
import { getSourceData, getTrackerData } from '../../helpers/trackers/trackers';
import Tooltip from '../ui/Tooltip';
import { captitalizeWords } from '../../helpers/helpers';
const renderLabel = (value: any) => (
<strong>
<Trans>{value}</Trans>
</strong>
);
interface renderLinkProps {
url: string;
name: string;
}
const renderLink = ({ url, name }: renderLinkProps) => (
<a className="tooltip-custom__content-link" target="_blank" rel="noopener noreferrer" href={url}>
<strong>{name}</strong>
</a>
);
const getTrackerInfo = (trackerData: any) => [
{
key: 'name_table_header',
value: trackerData,
render: renderLink,
},
{
key: 'category_label',
value: captitalizeWords(trackerData.category),
render: renderLabel,
},
{
key: 'source_label',
value: getSourceData(trackerData),
render: renderLink,
},
];
interface DomainCellProps {
value: string;
}
const DomainCell = ({ value }: DomainCellProps) => {
const trackerData = getTrackerData(value);
const content = trackerData && (
<div className="popover__list">
<div className="tooltip-custom__content-title mb-1">
<Trans>found_in_known_domain_db</Trans>
</div>
{getTrackerInfo(trackerData).map(({ key, value, render }) => (
<div key={key} className="tooltip-custom__content-item">
<Trans>{key}</Trans>: {render(value)}
</div>
))}
</div>
);
return (
<div className="logs__row">
<div className="logs__text" title={value}>
{value}
</div>
{trackerData && (
<Tooltip content={content} placement="top" className="tooltip-container tooltip-custom--wide">
<svg className="icons icon--24 icon--green ml-1">
<use xlinkHref="#privacy" />
</svg>
</Tooltip>
)}
</div>
);
};
export default DomainCell;

View File

@@ -1,6 +1,7 @@
import React from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next';
import Card from '../ui/Card';
@@ -8,9 +9,10 @@ import Cell from '../ui/Cell';
import DomainCell from './DomainCell';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
import { getPercent } from '../../helpers/helpers';
const getQueriedPercentColor = (percent) => {
const getQueriedPercentColor = (percent: any) => {
if (percent > 10) {
return STATUS_COLORS.red;
}
@@ -20,26 +22,27 @@ const getQueriedPercentColor = (percent) => {
return STATUS_COLORS.green;
};
const countCell = (dnsQueries) => function cell(row) {
const { value } = row;
const percent = getPercent(dnsQueries, value);
const percentColor = getQueriedPercentColor(percent);
const countCell = (dnsQueries: any) =>
function cell(row: any) {
const { value } = row;
const percent = getPercent(dnsQueries, value);
const percentColor = getQueriedPercentColor(percent);
return <Cell value={value} percent={percent} color={percentColor}
search={row.original.domain} />;
};
return <Cell value={value} percent={percent} color={percentColor} search={row.original.domain} />;
};
const QueriedDomains = ({
t, refreshButton, topQueriedDomains, subtitle, dnsQueries,
}) => (
<Card
title={t('stats_query_domain')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
interface QueriedDomainsProps {
topQueriedDomains: unknown[];
dnsQueries: number;
refreshButton: React.ReactNode;
subtitle: string;
t: (...args: unknown[]) => string;
}
const QueriedDomains = ({ t, refreshButton, topQueriedDomains, subtitle, dnsQueries }: QueriedDomainsProps) => (
<Card title={t('stats_query_domain')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
<ReactTable
data={topQueriedDomains.map(({ name: domain, count }) => ({
data={topQueriedDomains.map(({ name: domain, count }: any) => ({
domain,
count,
}))}
@@ -65,12 +68,4 @@ const QueriedDomains = ({
</Card>
);
QueriedDomains.propTypes = {
topQueriedDomains: PropTypes.array.isRequired,
dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(QueriedDomains);

View File

@@ -1,15 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { withTranslation, Trans } from 'react-i18next';
import StatsCard from './StatsCard';
import { getPercent, normalizeHistory } from '../../helpers/helpers';
import { RESPONSE_FILTER } from '../../helpers/constants';
const getNormalizedHistory = (data, interval, id) => [
{ data: normalizeHistory(data, interval), id },
];
const getNormalizedHistory = (data: any, interval: any, id: any) => [{ data: normalizeHistory(data), id }];
interface StatisticsProps {
interval: number;
dnsQueries: number[];
blockedFiltering: unknown[];
replacedSafebrowsing: unknown[];
replacedParental: unknown[];
numDnsQueries: number;
numBlockedFiltering: number;
numReplacedSafebrowsing: number;
numReplacedParental: number;
refreshButton: React.ReactNode;
}
const Statistics = ({
interval,
@@ -21,61 +33,68 @@ const Statistics = ({
numBlockedFiltering,
numReplacedSafebrowsing,
numReplacedParental,
}) => (
}: StatisticsProps) => (
<div className="row">
<div className="col-sm-6 col-lg-3">
<StatsCard
total={numDnsQueries}
lineData={getNormalizedHistory(dnsQueries, interval, 'dnsQuery')}
title={<Link to="logs"><Trans>dns_query</Trans></Link>}
title={
<Link to="logs">
<Trans>dns_query</Trans>
</Link>
}
color="blue"
/>
</div>
<div className="col-sm-6 col-lg-3">
<StatsCard
total={numBlockedFiltering}
lineData={getNormalizedHistory(blockedFiltering, interval, 'blockedFiltering')}
percent={getPercent(numDnsQueries, numBlockedFiltering)}
title={<Trans components={[<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED.QUERY}`} key="0">link</Link>]}>blocked_by</Trans>}
title={
<Trans
components={[
<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED.QUERY}`} key="0">
link
</Link>,
]}>
blocked_by
</Trans>
}
color="red"
/>
</div>
<div className="col-sm-6 col-lg-3">
<StatsCard
total={numReplacedSafebrowsing}
lineData={getNormalizedHistory(
replacedSafebrowsing,
interval,
'replacedSafebrowsing',
)}
lineData={getNormalizedHistory(replacedSafebrowsing, interval, 'replacedSafebrowsing')}
percent={getPercent(numDnsQueries, numReplacedSafebrowsing)}
title={<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_THREATS.QUERY}`}><Trans>stats_malware_phishing</Trans></Link>}
title={
<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_THREATS.QUERY}`}>
<Trans>stats_malware_phishing</Trans>
</Link>
}
color="green"
/>
</div>
<div className="col-sm-6 col-lg-3">
<StatsCard
total={numReplacedParental}
lineData={getNormalizedHistory(replacedParental, interval, 'replacedParental')}
percent={getPercent(numDnsQueries, numReplacedParental)}
title={<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY}`}><Trans>stats_adult</Trans></Link>}
title={
<Link to={`logs?response_status=${RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY}`}>
<Trans>stats_adult</Trans>
</Link>
}
color="yellow"
/>
</div>
</div>
);
Statistics.propTypes = {
interval: PropTypes.number.isRequired,
dnsQueries: PropTypes.array.isRequired,
blockedFiltering: PropTypes.array.isRequired,
replacedSafebrowsing: PropTypes.array.isRequired,
replacedParental: PropTypes.array.isRequired,
numDnsQueries: PropTypes.number.isRequired,
numBlockedFiltering: PropTypes.number.isRequired,
numReplacedSafebrowsing: PropTypes.number.isRequired,
numReplacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
};
export default withTranslation()(Statistics);

View File

@@ -1,38 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { STATUS_COLORS } from '../../helpers/constants';
import { formatNumber } from '../../helpers/helpers';
import Card from '../ui/Card';
import Line from '../ui/Line';
const StatsCard = ({
total, lineData, percent, title, color,
}) => (
interface StatsCardProps {
total: number;
lineData: unknown[];
title: object;
color: string;
percent?: number;
}
const StatsCard = ({ total, lineData, percent, title, color }: StatsCardProps) => (
<Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats">
<div className={`card-value card-value-stats text-${color}`}>
{formatNumber(total)}
</div>
<div className={`card-value card-value-stats text-${color}`}>{formatNumber(total)}</div>
<div className="card-title-stats">{title}</div>
</div>
{percent >= 0 && (
<div className={`card-value card-value-percent text-${color}`}>
{percent}
</div>
)}
{percent >= 0 && <div className={`card-value card-value-percent text-${color}`}>{percent}</div>}
<div className="card-chart-bg">
<Line data={lineData} color={STATUS_COLORS[color]} />
</div>
</Card>
);
StatsCard.propTypes = {
total: PropTypes.number.isRequired,
lineData: PropTypes.array.isRequired,
title: PropTypes.object.isRequired,
color: PropTypes.string.isRequired,
percent: PropTypes.number,
};
export default StatsCard;

View File

@@ -1,50 +1,47 @@
import React from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import round from 'lodash/round';
import { withTranslation, Trans } from 'react-i18next';
import { TFunction } from 'i18next';
import Card from '../ui/Card';
import DomainCell from './DomainCell';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, TABLES_MIN_ROWS } from '../../helpers/constants';
const TimeCell = ({ value }) => {
interface TimeCellProps {
value?: string | number;
}
const TimeCell = ({ value }: TimeCellProps) => {
if (!value) {
return '';
}
const valueInMilliseconds = round(value * 1000);
const valueInMilliseconds = round(Number(value) * 1000);
return (
<div className="logs__row o-hidden">
<span className="logs__text logs__text--full" title={valueInMilliseconds}>
<span className="logs__text logs__text--full" title={valueInMilliseconds.toString()}>
{valueInMilliseconds}&nbsp;ms
</span>
</div>
);
};
TimeCell.propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};
interface UpstreamAvgTimeProps {
topUpstreamsAvgTime: { name: string; count: number }[];
refreshButton: React.ReactNode;
subtitle: string;
t: TFunction;
}
const UpstreamAvgTime = ({
t,
refreshButton,
topUpstreamsAvgTime,
subtitle,
}) => (
<Card
title={t('average_upstream_response_time')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
const UpstreamAvgTime = ({ t, refreshButton, topUpstreamsAvgTime, subtitle }: UpstreamAvgTimeProps) => (
<Card title={t('average_upstream_response_time')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
<ReactTable
data={topUpstreamsAvgTime.map(({ name: domain, count }) => ({
data={topUpstreamsAvgTime.map(({ name: domain, count }: { name: string; count: number }) => ({
domain,
count,
}))}
@@ -70,11 +67,4 @@ const UpstreamAvgTime = ({
</Card>
);
UpstreamAvgTime.propTypes = {
topUpstreamsAvgTime: PropTypes.array.isRequired,
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(UpstreamAvgTime);

View File

@@ -1,51 +1,47 @@
import React from 'react';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next';
import { TFunction } from 'i18next';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
import DomainCell from './DomainCell';
import { getPercent } from '../../helpers/helpers';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
const CountCell = (totalBlocked) => (
function cell(row) {
const CountCell = (totalBlocked: any) =>
function cell(row: any) {
const { value } = row;
const percent = getPercent(totalBlocked, value);
return (
<Cell
value={value}
percent={percent}
color={STATUS_COLORS.green}
/>
);
}
);
return <Cell value={value} percent={percent} color={STATUS_COLORS.green} />;
};
const getTotalUpstreamRequests = (stats) => {
const getTotalUpstreamRequests = (stats: any) => {
let total = 0;
stats.forEach(({ count }) => { total += count; });
stats.forEach(({ count }: any) => {
total += count;
});
return total;
};
const UpstreamResponses = ({
t,
refreshButton,
topUpstreamsResponses,
subtitle,
}) => (
<Card
title={t('top_upstreams')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
interface UpstreamResponsesProps {
topUpstreamsResponses: { name: string; count: number }[];
refreshButton: React.ReactNode;
subtitle: string;
t: TFunction;
}
const UpstreamResponses = ({ t, refreshButton, topUpstreamsResponses, subtitle }: UpstreamResponsesProps) => (
<Card title={t('top_upstreams')} subtitle={subtitle} bodyType="card-table" refresh={refreshButton}>
<ReactTable
data={topUpstreamsResponses.map(({ name: domain, count }) => ({
data={topUpstreamsResponses.map(({ name: domain, count }: { name: string; count: number }) => ({
domain,
count,
}))}
@@ -71,11 +67,4 @@ const UpstreamResponses = ({
</Card>
);
UpstreamResponses.propTypes = {
topUpstreamsResponses: PropTypes.array.isRequired,
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation()(UpstreamResponses);

View File

@@ -1,276 +0,0 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { HashLink as Link } from 'react-router-hash-link';
import { Trans, useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Statistics from './Statistics';
import Counters from './Counters';
import Clients from './Clients';
import QueriedDomains from './QueriedDomains';
import BlockedDomains from './BlockedDomains';
import {
DISABLE_PROTECTION_TIMINGS,
ONE_SECOND_IN_MS,
SETTINGS_URLS,
TIME_UNITS,
} from '../../helpers/constants';
import {
msToSeconds,
msToMinutes,
msToHours,
msToDays,
} from '../../helpers/helpers';
import PageTitle from '../ui/PageTitle';
import Loading from '../ui/Loading';
import './Dashboard.css';
import Dropdown from '../ui/Dropdown';
import UpstreamResponses from './UpstreamResponses';
import UpstreamAvgTime from './UpstreamAvgTime';
const Dashboard = ({
getAccessList,
getStats,
getStatsConfig,
dashboard,
dashboard: { protectionEnabled, processingProtection, protectionDisabledDuration },
toggleProtection,
stats,
access,
}) => {
const { t } = useTranslation();
const getAllStats = () => {
getAccessList();
getStats();
getStatsConfig();
};
useEffect(() => {
getAllStats();
}, []);
const getSubtitle = () => {
if (!stats.enabled) {
return t('stats_disabled_short');
}
const msIn7Days = 604800000;
if (stats.timeUnits === TIME_UNITS.HOURS && stats.interval === msIn7Days) {
return t('for_last_days', { count: msToDays(stats.interval) });
}
return stats.timeUnits === TIME_UNITS.HOURS
? t('for_last_hours', { count: msToHours(stats.interval) })
: t('for_last_days', { count: msToDays(stats.interval) });
};
const buttonClass = classNames('btn btn-sm dashboard-protection-button', {
'btn-gray': protectionEnabled,
'btn-success': !protectionEnabled,
});
const refreshButton = <button
type="button"
className="btn btn-icon btn-outline-primary btn-sm"
title={t('refresh_btn')}
onClick={() => getAllStats()}
>
<svg className="icons icon12">
<use xlinkHref="#refresh" />
</svg>
</button>;
const statsProcessing = stats.processingStats
|| stats.processingGetConfig
|| access.processing;
const subtitle = getSubtitle();
const DISABLE_PROTECTION_ITEMS = [
{
text: t('disable_for_seconds', { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HALF_MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) }),
disableTime: DISABLE_PROTECTION_TIMINGS.TEN_MINUTES,
},
{
text: t('disable_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HOUR,
},
{
text: t('disable_until_tomorrow'),
disableTime: DISABLE_PROTECTION_TIMINGS.TOMORROW,
},
];
const getDisableProtectionItems = () => (
Object.values(DISABLE_PROTECTION_ITEMS)
.map((item, index) => (
<div
key={`disable_timings_${index}`}
className="dropdown-item"
onClick={() => {
toggleProtection(protectionEnabled, item.disableTime - ONE_SECOND_IN_MS);
}}
>
{item.text}
</div>
))
);
const getRemaningTimeText = (milliseconds) => {
if (!milliseconds) {
return '';
}
const date = new Date(milliseconds);
const hh = date.getUTCHours();
const mm = `0${date.getUTCMinutes()}`.slice(-2);
const ss = `0${date.getUTCSeconds()}`.slice(-2);
const formattedHH = `0${hh}`.slice(-2);
return hh ? `${formattedHH}:${mm}:${ss}` : `${mm}:${ss}`;
};
const getProtectionBtnText = (status) => (status ? t('disable_protection') : t('enable_protection'));
return <>
<PageTitle title={t('dashboard')} containerClass="page-title--dashboard">
<div className="page-title__protection">
<button
type="button"
className={buttonClass}
onClick={() => {
toggleProtection(protectionEnabled);
}}
disabled={processingProtection}
>
{protectionDisabledDuration
? `${t('enable_protection_timer')} ${getRemaningTimeText(protectionDisabledDuration)}`
: getProtectionBtnText(protectionEnabled)
}
</button>
{protectionEnabled && <Dropdown
label=""
baseClassName="dropdown-protection"
icon="arrow-down"
controlClassName="dropdown-protection__toggle"
menuClassName="dropdown-menu dropdown-menu-arrow dropdown-menu--protection"
>
{getDisableProtectionItems()}
</Dropdown>}
</div>
<button
type="button"
className="btn btn-outline-primary btn-sm"
onClick={getAllStats}
>
<Trans>refresh_statics</Trans>
</button>
</PageTitle>
{statsProcessing && <Loading />}
{!statsProcessing && <div className="row row-cards dashboard">
<div className="col-lg-12">
{stats.interval === 0 && (
<div className="alert alert-warning" role="alert">
<Trans components={[
<Link
to={`${SETTINGS_URLS.settings}#stats-config`}
key="0"
>
link
</Link>,
]}>
stats_disabled
</Trans>
</div>
)}
<Statistics
interval={msToDays(stats.interval)}
dnsQueries={stats.dnsQueries}
blockedFiltering={stats.blockedFiltering}
replacedSafebrowsing={stats.replacedSafebrowsing}
replacedParental={stats.replacedParental}
numDnsQueries={stats.numDnsQueries}
numBlockedFiltering={stats.numBlockedFiltering}
numReplacedSafebrowsing={stats.numReplacedSafebrowsing}
numReplacedParental={stats.numReplacedParental}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<Counters
subtitle={subtitle}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<Clients
subtitle={subtitle}
dnsQueries={stats.numDnsQueries}
topClients={stats.topClients}
clients={dashboard.clients}
autoClients={dashboard.autoClients}
refreshButton={refreshButton}
processingAccessSet={access.processingSet}
disallowedClients={access.disallowed_clients}
/>
</div>
<div className="col-lg-6">
<QueriedDomains
subtitle={subtitle}
dnsQueries={stats.numDnsQueries}
topQueriedDomains={stats.topQueriedDomains}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<BlockedDomains
subtitle={subtitle}
topBlockedDomains={stats.topBlockedDomains}
blockedFiltering={stats.numBlockedFiltering}
replacedSafebrowsing={stats.numReplacedSafebrowsing}
replacedSafesearch={stats.numReplacedSafesearch}
replacedParental={stats.numReplacedParental}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<UpstreamResponses
subtitle={subtitle}
topUpstreamsResponses={stats.topUpstreamsResponses}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<UpstreamAvgTime
subtitle={subtitle}
topUpstreamsAvgTime={stats.topUpstreamsAvgTime}
refreshButton={refreshButton}
/>
</div>
</div>}
</>;
};
Dashboard.propTypes = {
dashboard: PropTypes.object.isRequired,
stats: PropTypes.object.isRequired,
access: PropTypes.object.isRequired,
getStats: PropTypes.func.isRequired,
getStatsConfig: PropTypes.func.isRequired,
toggleProtection: PropTypes.func.isRequired,
getClients: PropTypes.func.isRequired,
getAccessList: PropTypes.func.isRequired,
};
export default Dashboard;

View File

@@ -0,0 +1,260 @@
import React, { useEffect } from 'react';
import { HashLink as Link } from 'react-router-hash-link';
import { Trans, useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Statistics from './Statistics';
import Counters from './Counters';
import Clients from './Clients';
import QueriedDomains from './QueriedDomains';
import BlockedDomains from './BlockedDomains';
import { DISABLE_PROTECTION_TIMINGS, ONE_SECOND_IN_MS, SETTINGS_URLS, TIME_UNITS } from '../../helpers/constants';
import { msToSeconds, msToMinutes, msToHours, msToDays } from '../../helpers/helpers';
import PageTitle from '../ui/PageTitle';
import Loading from '../ui/Loading';
import './Dashboard.css';
import Dropdown from '../ui/Dropdown';
import UpstreamResponses from './UpstreamResponses';
import UpstreamAvgTime from './UpstreamAvgTime';
import { AccessData, DashboardData, StatsData } from '../../initialState';
interface DashboardProps {
dashboard: DashboardData;
stats: StatsData;
access: AccessData;
getStats: (...args: unknown[]) => unknown;
getStatsConfig: (...args: unknown[]) => unknown;
toggleProtection: (...args: unknown[]) => unknown;
getClients: (...args: unknown[]) => unknown;
getAccessList: () => (dispatch: any) => void;
}
const Dashboard = ({
getAccessList,
getStats,
getStatsConfig,
dashboard: { protectionEnabled, processingProtection, protectionDisabledDuration },
toggleProtection,
stats,
access,
}: DashboardProps) => {
const { t } = useTranslation();
const getAllStats = () => {
getAccessList();
getStats();
getStatsConfig();
};
useEffect(() => {
getAllStats();
}, []);
const getSubtitle = () => {
if (!stats.enabled) {
return t('stats_disabled_short');
}
const msIn7Days = 604800000;
if (stats.timeUnits === TIME_UNITS.HOURS && stats.interval === msIn7Days) {
return t('for_last_days', { count: msToDays(stats.interval) });
}
return stats.timeUnits === TIME_UNITS.HOURS
? t('for_last_hours', { count: msToHours(stats.interval) })
: t('for_last_days', { count: msToDays(stats.interval) });
};
const buttonClass = classNames('btn btn-sm dashboard-protection-button', {
'btn-gray': protectionEnabled,
'btn-success': !protectionEnabled,
});
const refreshButton = (
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm"
title={t('refresh_btn')}
onClick={() => getAllStats()}>
<svg className="icons icon12">
<use xlinkHref="#refresh" />
</svg>
</button>
);
const statsProcessing = stats.processingStats || stats.processingGetConfig || access.processing;
const subtitle = getSubtitle();
const DISABLE_PROTECTION_ITEMS = [
{
text: t('disable_for_seconds', { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HALF_MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) }),
disableTime: DISABLE_PROTECTION_TIMINGS.TEN_MINUTES,
},
{
text: t('disable_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HOUR,
},
{
text: t('disable_until_tomorrow'),
disableTime: DISABLE_PROTECTION_TIMINGS.TOMORROW,
},
];
const getDisableProtectionItems = () =>
Object.values(DISABLE_PROTECTION_ITEMS).map((item: any, index: any) => (
<div
key={`disable_timings_${index}`}
className="dropdown-item"
onClick={() => {
toggleProtection(protectionEnabled, item.disableTime - ONE_SECOND_IN_MS);
}}>
{item.text}
</div>
));
const getRemaningTimeText = (milliseconds: any) => {
if (!milliseconds) {
return '';
}
const date = new Date(milliseconds);
const hh = date.getUTCHours();
const mm = `0${date.getUTCMinutes()}`.slice(-2);
const ss = `0${date.getUTCSeconds()}`.slice(-2);
const formattedHH = `0${hh}`.slice(-2);
return hh ? `${formattedHH}:${mm}:${ss}` : `${mm}:${ss}`;
};
const getProtectionBtnText = (status: any) => (status ? t('disable_protection') : t('enable_protection'));
return (
<>
<PageTitle title={t('dashboard')} containerClass="page-title--dashboard">
<div className="page-title__protection">
<button
type="button"
className={buttonClass}
onClick={() => {
toggleProtection(protectionEnabled);
}}
disabled={processingProtection}>
{protectionDisabledDuration
? `${t('enable_protection_timer')} ${getRemaningTimeText(protectionDisabledDuration)}`
: getProtectionBtnText(protectionEnabled)}
</button>
{protectionEnabled && (
<Dropdown
label=""
baseClassName="dropdown-protection"
icon="arrow-down"
controlClassName="dropdown-protection__toggle"
menuClassName="dropdown-menu dropdown-menu-arrow dropdown-menu--protection">
{getDisableProtectionItems()}
</Dropdown>
)}
</div>
<button type="button" className="btn btn-outline-primary btn-sm" onClick={getAllStats}>
<Trans>refresh_statics</Trans>
</button>
</PageTitle>
{statsProcessing && <Loading />}
{!statsProcessing && (
<div className="row row-cards dashboard">
<div className="col-lg-12">
{stats.interval === 0 && (
<div className="alert alert-warning" role="alert">
<Trans
components={[
<Link to={`${SETTINGS_URLS.settings}#stats-config`} key="0">
link
</Link>,
]}>
stats_disabled
</Trans>
</div>
)}
<Statistics
interval={msToDays(stats.interval)}
dnsQueries={stats.dnsQueries}
blockedFiltering={stats.blockedFiltering}
replacedSafebrowsing={stats.replacedSafebrowsing}
replacedParental={stats.replacedParental}
numDnsQueries={stats.numDnsQueries}
numBlockedFiltering={stats.numBlockedFiltering}
numReplacedSafebrowsing={stats.numReplacedSafebrowsing}
numReplacedParental={stats.numReplacedParental}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<Counters subtitle={subtitle} refreshButton={refreshButton} />
</div>
<div className="col-lg-6">
<Clients subtitle={subtitle} refreshButton={refreshButton} />
</div>
<div className="col-lg-6">
<QueriedDomains
subtitle={subtitle}
dnsQueries={stats.numDnsQueries}
topQueriedDomains={stats.topQueriedDomains}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<BlockedDomains
subtitle={subtitle}
topBlockedDomains={stats.topBlockedDomains}
blockedFiltering={stats.numBlockedFiltering}
replacedSafebrowsing={stats.numReplacedSafebrowsing}
replacedSafesearch={stats.numReplacedSafesearch}
replacedParental={stats.numReplacedParental}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<UpstreamResponses
subtitle={subtitle}
topUpstreamsResponses={stats.topUpstreamsResponses}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<UpstreamAvgTime
subtitle={subtitle}
topUpstreamsAvgTime={stats.topUpstreamsAvgTime}
refreshButton={refreshButton}
/>
</div>
</div>
)}
</>
);
};
export default Dashboard;