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:4c89cf7201085d59a6Author: Igor Lobanov <bniwredyc@gmail.com> Date: Mon Jun 10 17:22:20 2024 +0200 merge commit4c89cf7209Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jun 6 13:27:18 2024 +0300 remove install from initial state commitb943f2011fAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 23:10:55 2024 +0200 frontend production build fix commitcd1be2d66dAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 20:23:14 2024 +0200 production build quickfix commit7b8ac01fc2Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Wed Jun 5 19:57:31 2024 +0300 all: upd node docker commit02afed66d5Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 18:23:12 2024 +0200 changelog fixes commit9c0f736f0cMerge:62c4fbf1ee04775c4fAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 18:18:29 2024 +0200 merge commit62c4fbf1e3Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:22:22 2024 +0200 empty line in changelog commit76b1e44a93Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:20:37 2024 +0200 changelog commitf783e90040Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:19:13 2024 +0200 filters.js -> filters.ts commit3d4ce6554cAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:18:03 2024 +0200 generated file removed commite35ba58f2aAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 15:45:21 2024 +0200 rollback unwanted changes commit1f30d4216dAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 15:27:36 2024 +0200 review fix commit6cd4e44f07Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 11:55:39 2024 +0200 missing generated file restoresd commit2ab738b303Author: 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:
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
81
client/src/components/Dashboard/DomainCell.tsx
Normal file
81
client/src/components/Dashboard/DomainCell.tsx
Normal 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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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} 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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
260
client/src/components/Dashboard/index.tsx
Normal file
260
client/src/components/Dashboard/index.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user