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: 4c89cf720 1085d59a6
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Jun 10 17:22:20 2024 +0200

    merge

commit 4c89cf7209
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Jun 6 13:27:18 2024 +0300

    remove install from initial state

commit b943f2011f
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 23:10:55 2024 +0200

    frontend production build fix

commit cd1be2d66d
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 20:23:14 2024 +0200

    production build quickfix

commit 7b8ac01fc2
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 5 19:57:31 2024 +0300

    all: upd node docker

commit 02afed66d5
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 18:23:12 2024 +0200

    changelog fixes

commit 9c0f736f0c
Merge: 62c4fbf1e e04775c4f
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 18:18:29 2024 +0200

    merge

commit 62c4fbf1e3
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:22:22 2024 +0200

    empty line in changelog

commit 76b1e44a93
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:20:37 2024 +0200

    changelog

commit f783e90040
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:19:13 2024 +0200

    filters.js -> filters.ts

commit 3d4ce6554c
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:18:03 2024 +0200

    generated file removed

commit e35ba58f2a
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 15:45:21 2024 +0200

    rollback unwanted changes

commit 1f30d4216d
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 15:27:36 2024 +0200

    review fix

commit 6cd4e44f07
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 11:55:39 2024 +0200

    missing generated file restoresd

commit 2ab738b303
Author: 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:
Igor Lobanov
2024-06-10 18:42:23 +03:00
parent 1085d59a65
commit 1afe226ce8
296 changed files with 32122 additions and 32651 deletions

View File

@@ -0,0 +1,410 @@
/* 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 } 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 (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) => {
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: 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;