420 lines
14 KiB
TypeScript
420 lines
14 KiB
TypeScript
/* 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;
|