Files
AdGuardHome/client/src/components/Settings/Clients/ClientsTable/ClientsTable.tsx
Eugene Burkov 7f9cef948c Pull request 2358: Update all
Merge in DNS/adguard-home from upd-all to master

Squashed commit of the following:

commit fb5e87e0cb5617d031a2dac932304917722b1a89
Merge: af4ef937e 64994c7fc
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Mar 7 18:30:39 2025 +0300

    Merge branch 'master' into upd-all

commit af4ef937ee9ae1046cda083a4b0cb6b41ca3dc8c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Mar 7 12:50:48 2025 +0300

    all: log changes, revert trackers

commit ca1197dc69bb845daa5ec3f25d58d995f3d330ef
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Mar 6 19:33:24 2025 +0300

    client: upd i18n

commit d6aa69668633afcabdf956a3e82b920077ddbc75
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Mar 6 19:24:57 2025 +0300

    client: upd vetted filters, companiesdb, blocked services

commit ed6f706c8eafe248b1e851bb0b123f1c46795414
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Mar 6 19:20:32 2025 +0300

    ipset: add bench results

commit 89c1fbe257163aebcddd1abf85b8e4d87536e0ef
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Mar 6 19:14:34 2025 +0300

    all: upd go, tools
2025-03-07 18:51:31 +03:00

420 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable react/display-name */
/* eslint-disable react/prop-types */
import React, { useEffect } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import { getAllBlockedServices, getBlockedServices } from '../../../../actions/services';
import { initSettings } from '../../../../actions';
import { splitByNewLine, countClientsStatistics, sortIp, getService, formatNumber } from '../../../../helpers/helpers';
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE, TABLES_MIN_ROWS } from '../../../../helpers/constants';
import Card from '../../../ui/Card';
import CellWrap from '../../../ui/CellWrap';
import LogsSearchLink from '../../../ui/LogsSearchLink';
import Modal from '../Modal';
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../../helpers/localStorageHelper';
import { Client, NormalizedTopClients, RootState } from '../../../../initialState';
interface ClientsTableProps {
clients: Client[];
normalizedTopClients: NormalizedTopClients;
toggleClientModal: (...args: unknown[]) => unknown;
deleteClient: (...args: unknown[]) => string;
addClient: (...args: unknown[]) => string;
updateClient: (...args: unknown[]) => string;
isModalOpen: boolean;
modalType: string;
modalClientName: string;
processingAdding: boolean;
processingDeleting: boolean;
processingUpdating: boolean;
getStats: (...args: unknown[]) => unknown;
supportedTags: string[];
}
const ClientsTable = ({
clients,
normalizedTopClients,
isModalOpen,
modalClientName,
modalType,
addClient,
updateClient,
deleteClient,
toggleClientModal,
processingAdding,
processingDeleting,
processingUpdating,
getStats,
supportedTags,
}: ClientsTableProps) => {
const [t] = useTranslation();
const dispatch = useDispatch();
const location = useLocation();
const history = useHistory();
const services = useSelector((state: RootState) => state?.services);
const globalSettings = useSelector((state: RootState) => state?.settings.settingsList);
const params = new URLSearchParams(location.search);
const clientId = params.get('clientId');
useEffect(() => {
dispatch(getAllBlockedServices());
dispatch(getBlockedServices());
dispatch(initSettings());
if (clientId) {
toggleClientModal({
type: MODAL_TYPE.ADD_CLIENT,
});
}
}, []);
const handleFormAdd = (values: any) => {
addClient(values);
};
const handleFormUpdate = (values: any, name: any) => {
updateClient(values, name);
};
const handleSubmit = (values: any) => {
const config = { ...values };
if (values) {
if (values.blocked_services) {
config.blocked_services = Object.keys(values.blocked_services).filter(
(service) => values.blocked_services[service],
);
}
if (values.upstreams && typeof values.upstreams === 'string') {
config.upstreams = splitByNewLine(values.upstreams);
} else {
config.upstreams = [];
}
if (values.tags) {
config.tags = values.tags.map((tag: any) => tag.value);
} else {
config.tags = [];
}
if (values.ids) {
config.ids = values.ids.map((id) => id.name);
} else {
config.ids = [];
}
if (typeof values.upstreams_cache_size === 'string') {
config.upstreams_cache_size = 0;
}
}
if (modalType === MODAL_TYPE.EDIT_CLIENT) {
handleFormUpdate(config, modalClientName);
} else {
handleFormAdd(config);
}
if (clientId) {
history.push('/#clients');
}
};
const getOptionsWithLabels = (options: any) =>
options.map((option: any) => ({
value: option,
label: option,
}));
const getClient = (name: any, clients: any) => {
const client = clients.find((item: any) => name === item.name);
if (client) {
const { upstreams, tags, ...values } = client;
return {
upstreams: (upstreams && upstreams.join('\n')) || '',
tags: (tags && getOptionsWithLabels(tags)) || [],
...values,
};
}
return {
ids: [''],
tags: [],
use_global_settings: true,
use_global_blocked_services: true,
blocked_services_schedule: {
time_zone: LOCAL_TIMEZONE_VALUE,
},
safe_search: { ...(globalSettings?.safesearch || {}) },
};
};
const handleDelete = (data: any) => {
// eslint-disable-next-line no-alert
if (window.confirm(t('client_confirm_delete', { key: data.name }))) {
deleteClient(data);
getStats();
}
};
const handleClose = () => {
toggleClientModal();
if (clientId) {
history.push('/#clients');
}
};
const columns = [
{
Header: t('table_client'),
accessor: 'ids',
minWidth: 150,
Cell: (row: any) => {
const { value } = row;
return (
<div className="logs__row o-hidden">
<span className="logs__text">
{value.map((address: any) => (
<div key={address} title={address}>
{address}
</div>
))}
</span>
</div>
);
},
sortMethod: sortIp,
},
{
Header: t('table_name'),
accessor: 'name',
minWidth: 120,
Cell: CellWrap,
},
{
Header: t('settings'),
accessor: 'use_global_settings',
minWidth: 120,
Cell: ({ value }: any) => {
const title = value ? <Trans>settings_global</Trans> : <Trans>settings_custom</Trans>;
return (
<div className="logs__row o-hidden">
<div className="logs__text">{title}</div>
</div>
);
},
},
{
Header: t('blocked_services'),
accessor: 'blocked_services',
minWidth: 180,
Cell: (row: any) => {
const { value, original } = row;
if (original.use_global_blocked_services) {
return <Trans>settings_global</Trans>;
}
if (value && services.allServices) {
return (
<div className="logs__row logs__row--icons">
{value.map((service: any) => {
const serviceInfo = getService(services.allServices, service);
if (serviceInfo?.icon_svg) {
return (
<div
key={serviceInfo.name}
dangerouslySetInnerHTML={{
__html: window.atob(serviceInfo.icon_svg),
}}
className="service__icon service__icon--table"
title={serviceInfo.name}
/>
);
}
return null;
})}
</div>
);
}
return <div className="logs__row logs__row--icons"></div>;
},
},
{
Header: t('upstreams'),
accessor: 'upstreams',
minWidth: 120,
Cell: ({ value }: any) => {
const title =
value && value.length > 0 ? <Trans>settings_custom</Trans> : <Trans>settings_global</Trans>;
return (
<div className="logs__row o-hidden">
<div className="logs__text">{title}</div>
</div>
);
},
},
{
Header: t('tags_title'),
accessor: 'tags',
minWidth: 140,
Cell: (row: any) => {
const { value } = row;
if (!value || value.length < 1) {
return '';
}
return (
<div className="logs__row o-hidden">
<span className="logs__text">
{value.map((tag: any) => (
<div key={tag} title={tag} className="logs__tag small">
{tag}
</div>
))}
</span>
</div>
);
},
},
{
Header: t('requests_count'),
id: 'statistics',
accessor: (row: any) => countClientsStatistics(row.ids, normalizedTopClients.auto),
sortMethod: (a: any, b: any) => b - a,
minWidth: 120,
Cell: (row: any) => {
let content = row.value;
if (typeof content === "number") {
content = formatNumber(content);
} else {
content = CellWrap(row);
}
if (!content) {
return content;
}
return <LogsSearchLink search={row.original.name}>{content}</LogsSearchLink>;
},
},
{
Header: t('actions_table_header'),
accessor: 'actions',
maxWidth: 100,
sortable: false,
resizable: false,
Cell: (row: any) => {
const clientName = row.original.name;
return (
<div className="logs__row logs__row--center">
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
onClick={() =>
toggleClientModal({
type: MODAL_TYPE.EDIT_CLIENT,
name: clientName,
})
}
disabled={processingUpdating}
title={t('edit_table_action')}>
<svg className="icons icon12">
<use xlinkHref="#edit" />
</svg>
</button>
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => handleDelete({ name: clientName })}
disabled={processingDeleting}
title={t('delete_table_action')}>
<svg className="icons icon12">
<use xlinkHref="#delete" />
</svg>
</button>
</div>
);
},
},
];
const currentClientData = getClient(modalClientName, clients);
const tagsOptions = getOptionsWithLabels(supportedTags);
return (
<Card title={t('clients_title')} subtitle={t('clients_desc')} bodyType="card-body box-body--settings">
<>
<ReactTable
data={clients || []}
columns={columns}
defaultSorted={[
{
id: 'statistics',
asc: true,
},
]}
className="-striped -highlight card-table-overflow"
showPagination
defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE) || 10}
onPageSizeChange={(size: any) =>
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE, size)
}
minRows={TABLES_MIN_ROWS}
ofText="/"
previousText={t('previous_btn')}
nextText={t('next_btn')}
pageText={t('page_table_footer_text')}
rowsText={t('rows_table_footer_text')}
loadingText={t('loading_table_status')}
noDataText={t('clients_not_found')}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleClientModal(MODAL_TYPE.ADD_FILTERS)}
disabled={processingAdding}>
<Trans>client_add</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
modalType={modalType}
handleClose={handleClose}
currentClientData={currentClientData}
handleSubmit={handleSubmit}
processingAdding={processingAdding}
processingUpdating={processingUpdating}
tagsOptions={tagsOptions}
clientId={clientId}
/>
</>
</Card>
);
};
export default ClientsTable;