Updates #3972.
Squashed commit of the following:
commit 9dc0efe2453cb6c738d97d39b02c86eccb18a42c
Merge: 239550f8 8a935d4f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Thu Oct 27 14:42:38 2022 +0300
Merge branch 'master' into 3972-hostlists-services
commit 239550f84228e7c7a6f4ae6b1cadcc47e01f54d5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Thu Oct 27 14:41:42 2022 +0300
filtering: upd service list
commit b8bf3a6a4b1333059b886be95a1419612aebac39
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Oct 27 13:41:09 2022 +0300
client: remove todo
commit caa504b482befb804db2a1ca0b6d4834aa4da49a
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Oct 27 12:54:45 2022 +0300
fix build
commit 511797c305d9eef84a20553dab795414e00da51a
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Oct 27 12:40:33 2022 +0300
client: add titles with service names to the clients table
commit 79ed3157a85b489a0b13381cff867a8c73ba60e9
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Oct 27 12:36:59 2022 +0300
client: fix empty icons
commit ab69b95784de87665d5a1a3683f28e3b3df1c210
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Oct 27 11:55:48 2022 +0300
client: use all blocked services
commit 9a4a87665c8463224d8e93f1e162988107f6c7ca
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Tue Oct 25 19:25:20 2022 +0300
all: fix json response
commit 86eb4493ce305cd5991176bd4cd8f7f5afdea330
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date: Tue Oct 25 19:09:44 2022 +0300
all: use hostslists registry for blocked svcs
386 lines
12 KiB
JavaScript
386 lines
12 KiB
JavaScript
/* eslint-disable react/display-name */
|
||
/* eslint-disable react/prop-types */
|
||
import React, { useEffect } from 'react';
|
||
import PropTypes from 'prop-types';
|
||
import { Trans, useTranslation } from 'react-i18next';
|
||
import { useDispatch, useSelector } from 'react-redux';
|
||
import ReactTable from 'react-table';
|
||
|
||
import { getAllBlockedServices } from '../../../../actions/services';
|
||
import {
|
||
splitByNewLine,
|
||
countClientsStatistics,
|
||
sortIp,
|
||
getService,
|
||
} from '../../../../helpers/helpers';
|
||
import { MODAL_TYPE } from '../../../../helpers/constants';
|
||
import Card from '../../../ui/Card';
|
||
import CellWrap from '../../../ui/CellWrap';
|
||
import LogsSearchLink from '../../../ui/LogsSearchLink';
|
||
import Modal from '../Modal';
|
||
|
||
const ClientsTable = ({
|
||
clients,
|
||
normalizedTopClients,
|
||
isModalOpen,
|
||
modalClientName,
|
||
modalType,
|
||
addClient,
|
||
updateClient,
|
||
deleteClient,
|
||
toggleClientModal,
|
||
processingAdding,
|
||
processingDeleting,
|
||
processingUpdating,
|
||
getStats,
|
||
supportedTags,
|
||
}) => {
|
||
const [t] = useTranslation();
|
||
const dispatch = useDispatch();
|
||
const services = useSelector((store) => store?.services);
|
||
|
||
useEffect(() => {
|
||
dispatch(getAllBlockedServices());
|
||
}, []);
|
||
|
||
const handleFormAdd = (values) => {
|
||
addClient(values);
|
||
};
|
||
|
||
const handleFormUpdate = (values, name) => {
|
||
updateClient(values, name);
|
||
};
|
||
|
||
const handleSubmit = (values) => {
|
||
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) => tag.value);
|
||
} else {
|
||
config.tags = [];
|
||
}
|
||
}
|
||
|
||
if (modalType === MODAL_TYPE.EDIT_FILTERS) {
|
||
handleFormUpdate(config, modalClientName);
|
||
} else {
|
||
handleFormAdd(config);
|
||
}
|
||
};
|
||
|
||
const getOptionsWithLabels = (options) => (
|
||
options.map((option) => ({
|
||
value: option,
|
||
label: option,
|
||
}))
|
||
);
|
||
|
||
const getClient = (name, clients) => {
|
||
const client = clients.find((item) => name === item.name);
|
||
|
||
if (client) {
|
||
const {
|
||
upstreams, tags, whois_info, ...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,
|
||
};
|
||
};
|
||
|
||
const handleDelete = (data) => {
|
||
// eslint-disable-next-line no-alert
|
||
if (window.confirm(t('client_confirm_delete', { key: data.name }))) {
|
||
deleteClient(data);
|
||
getStats();
|
||
}
|
||
};
|
||
|
||
const columns = [
|
||
{
|
||
Header: t('table_client'),
|
||
accessor: 'ids',
|
||
minWidth: 150,
|
||
Cell: (row) => {
|
||
const { value } = row;
|
||
|
||
return (
|
||
<div className="logs__row o-hidden">
|
||
<span className="logs__text">
|
||
{value.map((address) => (
|
||
<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 }) => {
|
||
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) => {
|
||
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) => {
|
||
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 }) => {
|
||
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) => {
|
||
const { value } = row;
|
||
|
||
if (!value || value.length < 1) {
|
||
return '–';
|
||
}
|
||
|
||
return (
|
||
<div className="logs__row o-hidden">
|
||
<span className="logs__text">
|
||
{value.map((tag) => (
|
||
<div key={tag} title={tag} className="logs__tag small">
|
||
{tag}
|
||
</div>
|
||
))}
|
||
</span>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
Header: t('requests_count'),
|
||
id: 'statistics',
|
||
accessor: (row) => countClientsStatistics(
|
||
row.ids,
|
||
normalizedTopClients.auto,
|
||
),
|
||
sortMethod: (a, b) => b - a,
|
||
minWidth: 120,
|
||
Cell: (row) => {
|
||
const content = CellWrap(row);
|
||
|
||
if (!row.value) {
|
||
return content;
|
||
}
|
||
|
||
return <LogsSearchLink search={row.original.ids[0]}>{content}</LogsSearchLink>;
|
||
},
|
||
},
|
||
{
|
||
Header: t('actions_table_header'),
|
||
accessor: 'actions',
|
||
maxWidth: 100,
|
||
sortable: false,
|
||
resizable: false,
|
||
Cell: (row) => {
|
||
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_FILTERS,
|
||
name: clientName,
|
||
})
|
||
}
|
||
disabled={processingUpdating}
|
||
title={t('edit_table_action')}
|
||
>
|
||
<svg className="icons">
|
||
<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">
|
||
<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={10}
|
||
minRows={5}
|
||
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}
|
||
toggleClientModal={toggleClientModal}
|
||
currentClientData={currentClientData}
|
||
handleSubmit={handleSubmit}
|
||
processingAdding={processingAdding}
|
||
processingUpdating={processingUpdating}
|
||
tagsOptions={tagsOptions}
|
||
/>
|
||
</>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
ClientsTable.propTypes = {
|
||
clients: PropTypes.array.isRequired,
|
||
normalizedTopClients: PropTypes.object.isRequired,
|
||
toggleClientModal: PropTypes.func.isRequired,
|
||
deleteClient: PropTypes.func.isRequired,
|
||
addClient: PropTypes.func.isRequired,
|
||
updateClient: PropTypes.func.isRequired,
|
||
isModalOpen: PropTypes.bool.isRequired,
|
||
modalType: PropTypes.string.isRequired,
|
||
modalClientName: PropTypes.string.isRequired,
|
||
processingAdding: PropTypes.bool.isRequired,
|
||
processingDeleting: PropTypes.bool.isRequired,
|
||
processingUpdating: PropTypes.bool.isRequired,
|
||
getStats: PropTypes.func.isRequired,
|
||
supportedTags: PropTypes.array.isRequired,
|
||
};
|
||
|
||
export default ClientsTable;
|