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
208 lines
6.9 KiB
TypeScript
208 lines
6.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
|
|
// @ts-expect-error FIXME: update react-table
|
|
import ReactTable from 'react-table';
|
|
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';
|
|
|
|
import { getPercent, sortIp } from '../../helpers/helpers';
|
|
import {
|
|
BLOCK_ACTIONS,
|
|
DASHBOARD_TABLES_DEFAULT_PAGE_SIZE,
|
|
STATUS_COLORS,
|
|
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';
|
|
import { RootState } from '../../initialState';
|
|
|
|
const getClientsPercentColor = (percent: any) => {
|
|
if (percent > 50) {
|
|
return STATUS_COLORS.green;
|
|
}
|
|
if (percent > 10) {
|
|
return STATUS_COLORS.yellow;
|
|
}
|
|
return STATUS_COLORS.red;
|
|
};
|
|
|
|
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);
|
|
|
|
return <Cell value={value} percent={percent} color={percentColor} search={ip} />;
|
|
};
|
|
|
|
const renderBlockingButton = (ip: any, disallowed: any, disallowed_rule: any) => {
|
|
const dispatch = useDispatch();
|
|
const { t } = useTranslation();
|
|
|
|
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: 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 (allowedClients.length > 0) {
|
|
confirmMessage = confirmMessage.concat(`\n\n${t('filter_allowlist', { disallowed_rule })}`);
|
|
}
|
|
}
|
|
|
|
if (window.confirm(confirmMessage)) {
|
|
await dispatch(toggleClientBlock(ip, disallowed, disallowed_rule));
|
|
await dispatch(getStats());
|
|
}
|
|
};
|
|
|
|
const onClick = () => {
|
|
toggleClientStatus(ip, disallowed, disallowed_rule);
|
|
setOptionsOpened(false);
|
|
};
|
|
|
|
const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
|
|
|
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)}>
|
|
<svg className="icon24 icon--lightgray button-action__icon">
|
|
<use xlinkHref="#bullets" />
|
|
</svg>
|
|
</button>
|
|
{isOptionsOpened && (
|
|
<IconTooltip
|
|
className="icon24"
|
|
tooltipClass="button-action--arrow-option-container"
|
|
xlinkHref="bullets"
|
|
triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
|
|
content={
|
|
<button
|
|
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 }) : ''}>
|
|
<Trans>{text}</Trans>
|
|
</button>
|
|
}
|
|
placement="bottom-end"
|
|
trigger="click"
|
|
onVisibilityChange={setOptionsOpened}
|
|
defaultTooltipShown={true}
|
|
delayHide={0}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ClientCell = (row: any) => {
|
|
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>
|
|
</>
|
|
);
|
|
};
|
|
|
|
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 }: any) => ({
|
|
ip,
|
|
count,
|
|
info,
|
|
blocked,
|
|
}))}
|
|
columns={[
|
|
{
|
|
Header: <Trans>client_table_header</Trans>,
|
|
accessor: 'ip',
|
|
sortMethod: sortIp,
|
|
Cell: ClientCell,
|
|
},
|
|
{
|
|
Header: <Trans>requests_count</Trans>,
|
|
accessor: 'count',
|
|
minWidth: 180,
|
|
maxWidth: 200,
|
|
Cell: CountCell,
|
|
},
|
|
]}
|
|
showPagination={false}
|
|
noDataText={t('no_clients_found')}
|
|
minRows={TABLES_MIN_ROWS}
|
|
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
|
|
className="-highlight card-table-overflow--limited clients__table"
|
|
getTrProps={(_state: any, rowInfo: any) => {
|
|
if (!rowInfo) {
|
|
return {};
|
|
}
|
|
|
|
const {
|
|
info: { disallowed },
|
|
} = rowInfo.original;
|
|
|
|
return disallowed ? { className: 'logs__row--red' } : {};
|
|
}}
|
|
/>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default Clients;
|