all: sync with master

This commit is contained in:
Ainar Garipov
2024-07-03 15:38:37 +03:00
parent f73717ec08
commit 158d4f0249
352 changed files with 33842 additions and 33276 deletions

View File

@@ -1,20 +1,30 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
// @ts-expect-error FIXME: update react-table
import ReactTable from 'react-table';
import Card from '../../ui/Card';
import CellWrap from '../../ui/CellWrap';
import whoisCell from './whoisCell';
import LogsSearchLink from '../../ui/LogsSearchLink';
import { sortIp } from '../../../helpers/helpers';
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
import { TABLES_MIN_ROWS } from '../../../helpers/constants';
const COLUMN_MIN_WIDTH = 200;
class AutoClients extends Component {
interface AutoClientsProps {
t: (...args: unknown[]) => string;
autoClients: any[];
normalizedTopClients: any;
}
class AutoClients extends Component<AutoClientsProps> {
columns = [
{
Header: this.props.t('table_client'),
@@ -39,24 +49,24 @@ class AutoClients extends Component {
Header: this.props.t('whois'),
accessor: 'whois_info',
minWidth: COLUMN_MIN_WIDTH,
Cell: whoisCell(this.props.t),
},
{
Header: this.props.t('requests_count'),
accessor: (row) => this.props.normalizedTopClients.auto[row.ip] || 0,
sortMethod: (a, b) => b - a,
accessor: (row: any) => this.props.normalizedTopClients.auto[row.ip] || 0,
sortMethod: (a: any, b: any) => b - a,
id: 'statistics',
minWidth: COLUMN_MIN_WIDTH,
Cell: (row) => {
Cell: (row: any) => {
const { value: clientStats } = row;
if (clientStats) {
return (
<div className="logs__row">
<div className="logs__text" title={clientStats}>
<LogsSearchLink search={row.original.ip}>
{clientStats}
</LogsSearchLink>
<LogsSearchLink search={row.original.ip}>{clientStats}</LogsSearchLink>
</div>
</div>
);
@@ -74,8 +84,7 @@ class AutoClients extends Component {
<Card
title={t('auto_clients_title')}
subtitle={t('auto_clients_desc')}
bodyType="card-body box-body--settings"
>
bodyType="card-body box-body--settings">
<ReactTable
data={autoClients || []}
columns={this.columns}
@@ -88,9 +97,9 @@ class AutoClients extends Component {
className="-striped -highlight card-table-overflow"
showPagination
defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE) || 10}
onPageSizeChange={(size) => (
onPageSizeChange={(size: any) =>
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE, size)
)}
}
minRows={TABLES_MIN_ROWS}
ofText="/"
previousText={t('previous_btn')}
@@ -105,10 +114,4 @@ class AutoClients extends Component {
}
}
AutoClients.propTypes = {
t: PropTypes.func.isRequired,
autoClients: PropTypes.array.isRequired,
normalizedTopClients: PropTypes.object.isRequired,
};
export default withTranslation()(AutoClients);

View File

@@ -1,26 +1,46 @@
/* 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 { 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 { 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,
@@ -37,18 +57,18 @@ const ClientsTable = ({
processingUpdating,
getStats,
supportedTags,
}) => {
}: ClientsTableProps) => {
const [t] = useTranslation();
const dispatch = useDispatch();
const location = useLocation();
const history = useHistory();
const services = useSelector((store) => store?.services);
const globalSettings = useSelector((store) => store?.settings.settingsList) || {};
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');
const { safesearch } = globalSettings;
useEffect(() => {
dispatch(getAllBlockedServices());
dispatch(getBlockedServices());
@@ -61,22 +81,22 @@ const ClientsTable = ({
}
}, []);
const handleFormAdd = (values) => {
const handleFormAdd = (values: any) => {
addClient(values);
};
const handleFormUpdate = (values, name) => {
const handleFormUpdate = (values: any, name: any) => {
updateClient(values, name);
};
const handleSubmit = (values) => {
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]);
config.blocked_services = Object.keys(values.blocked_services).filter(
(service) => values.blocked_services[service],
);
}
if (values.upstreams && typeof values.upstreams === 'string') {
@@ -86,7 +106,7 @@ const ClientsTable = ({
}
if (values.tags) {
config.tags = values.tags.map((tag) => tag.value);
config.tags = values.tags.map((tag: any) => tag.value);
} else {
config.tags = [];
}
@@ -107,20 +127,17 @@ const ClientsTable = ({
}
};
const getOptionsWithLabels = (options) => (
options.map((option) => ({
const getOptionsWithLabels = (options: any) =>
options.map((option: any) => ({
value: option,
label: option,
}))
);
}));
const getClient = (name, clients) => {
const client = clients.find((item) => name === item.name);
const getClient = (name: any, clients: any) => {
const client = clients.find((item: any) => name === item.name);
if (client) {
const {
upstreams, tags, whois_info, ...values
} = client;
const { upstreams, tags, ...values } = client;
return {
upstreams: (upstreams && upstreams.join('\n')) || '',
tags: (tags && getOptionsWithLabels(tags)) || [],
@@ -136,11 +153,11 @@ const ClientsTable = ({
blocked_services_schedule: {
time_zone: LOCAL_TIMEZONE_VALUE,
},
safe_search: { ...(safesearch || {}) },
safe_search: { ...(globalSettings?.safesearch || {}) },
};
};
const handleDelete = (data) => {
const handleDelete = (data: any) => {
// eslint-disable-next-line no-alert
if (window.confirm(t('client_confirm_delete', { key: data.name }))) {
deleteClient(data);
@@ -161,13 +178,13 @@ const ClientsTable = ({
Header: t('table_client'),
accessor: 'ids',
minWidth: 150,
Cell: (row) => {
Cell: (row: any) => {
const { value } = row;
return (
<div className="logs__row o-hidden">
<span className="logs__text">
{value.map((address) => (
{value.map((address: any) => (
<div key={address} title={address}>
{address}
</div>
@@ -188,12 +205,8 @@ const ClientsTable = ({
Header: t('settings'),
accessor: 'use_global_settings',
minWidth: 120,
Cell: ({ value }) => {
const title = value ? (
<Trans>settings_global</Trans>
) : (
<Trans>settings_custom</Trans>
);
Cell: ({ value }: any) => {
const title = value ? <Trans>settings_global</Trans> : <Trans>settings_custom</Trans>;
return (
<div className="logs__row o-hidden">
@@ -206,7 +219,7 @@ const ClientsTable = ({
Header: t('blocked_services'),
accessor: 'blocked_services',
minWidth: 180,
Cell: (row) => {
Cell: (row: any) => {
const { value, original } = row;
if (original.use_global_blocked_services) {
@@ -216,7 +229,7 @@ const ClientsTable = ({
if (value && services.allServices) {
return (
<div className="logs__row logs__row--icons">
{value.map((service) => {
{value.map((service: any) => {
const serviceInfo = getService(services.allServices, service);
if (serviceInfo?.icon_svg) {
@@ -238,23 +251,16 @@ const ClientsTable = ({
);
}
return (
<div className="logs__row logs__row--icons">
</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>
);
Cell: ({ value }: any) => {
const title =
value && value.length > 0 ? <Trans>settings_custom</Trans> : <Trans>settings_global</Trans>;
return (
<div className="logs__row o-hidden">
@@ -267,7 +273,7 @@ const ClientsTable = ({
Header: t('tags_title'),
accessor: 'tags',
minWidth: 140,
Cell: (row) => {
Cell: (row: any) => {
const { value } = row;
if (!value || value.length < 1) {
@@ -277,7 +283,7 @@ const ClientsTable = ({
return (
<div className="logs__row o-hidden">
<span className="logs__text">
{value.map((tag) => (
{value.map((tag: any) => (
<div key={tag} title={tag} className="logs__tag small">
{tag}
</div>
@@ -290,13 +296,10 @@ const ClientsTable = ({
{
Header: t('requests_count'),
id: 'statistics',
accessor: (row) => countClientsStatistics(
row.ids,
normalizedTopClients.auto,
),
sortMethod: (a, b) => b - a,
accessor: (row: any) => countClientsStatistics(row.ids, normalizedTopClients.auto),
sortMethod: (a: any, b: any) => b - a,
minWidth: 120,
Cell: (row) => {
Cell: (row: any) => {
const content = CellWrap(row);
if (!row.value) {
@@ -312,7 +315,7 @@ const ClientsTable = ({
maxWidth: 100,
sortable: false,
resizable: false,
Cell: (row) => {
Cell: (row: any) => {
const clientName = row.original.name;
return (
@@ -320,25 +323,25 @@ const ClientsTable = ({
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
onClick={() => toggleClientModal({
type: MODAL_TYPE.EDIT_CLIENT,
name: clientName,
})
onClick={() =>
toggleClientModal({
type: MODAL_TYPE.EDIT_CLIENT,
name: clientName,
})
}
disabled={processingUpdating}
title={t('edit_table_action')}
>
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')}
>
title={t('delete_table_action')}>
<svg className="icons icon12">
<use xlinkHref="#delete" />
</svg>
@@ -353,11 +356,7 @@ const ClientsTable = ({
const tagsOptions = getOptionsWithLabels(supportedTags);
return (
<Card
title={t('clients_title')}
subtitle={t('clients_desc')}
bodyType="card-body box-body--settings"
>
<Card title={t('clients_title')} subtitle={t('clients_desc')} bodyType="card-body box-body--settings">
<>
<ReactTable
data={clients || []}
@@ -371,9 +370,9 @@ const ClientsTable = ({
className="-striped -highlight card-table-overflow"
showPagination
defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE) || 10}
onPageSizeChange={(size) => (
onPageSizeChange={(size: any) =>
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE, size)
)}
}
minRows={TABLES_MIN_ROWS}
ofText="/"
previousText={t('previous_btn')}
@@ -383,14 +382,15 @@ const ClientsTable = ({
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}
>
disabled={processingAdding}>
<Trans>client_add</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
modalType={modalType}
@@ -407,21 +407,4 @@ const ClientsTable = ({
);
};
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;

View File

@@ -1,491 +0,0 @@
import React, { useState } from 'react';
import { connect, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import {
Field, FieldArray, reduxForm, formValueSelector,
} from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import Select from 'react-select';
import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs';
import Examples from '../Dns/Upstream/Examples';
import { ScheduleForm } from '../../Filters/Services/ScheduleForm';
import {
toggleAllServices,
trimLinesAndRemoveEmpty,
captitalizeWords,
} from '../../../helpers/helpers';
import {
toNumber,
renderInputField,
renderGroupField,
CheckboxField,
renderServiceField,
renderTextareaField,
} from '../../../helpers/form';
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
import { CLIENT_ID_LINK, FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import './Service.css';
const settingsCheckboxes = [
{
name: 'use_global_settings',
placeholder: 'client_global_settings',
},
{
name: 'filtering_enabled',
placeholder: 'block_domain_use_filters_and_hosts',
},
{
name: 'safebrowsing_enabled',
placeholder: 'use_adguard_browsing_sec',
},
{
name: 'parental_enabled',
placeholder: 'use_adguard_parental',
},
];
const logAndStatsCheckboxes = [
{
name: 'ignore_querylog',
placeholder: 'ignore_query_log',
},
{
name: 'ignore_statistics',
placeholder: 'ignore_statistics',
},
];
const validate = (values) => {
const errors = {};
const { name, ids } = values;
errors.name = validateRequiredValue(name);
if (ids && ids.length) {
const idArrayErrors = [];
ids.forEach((id, idx) => {
idArrayErrors[idx] = validateRequiredValue(id) || validateClientId(id);
});
if (idArrayErrors.length) {
errors.ids = idArrayErrors;
}
}
return errors;
};
const renderFieldsWrapper = (placeholder, buttonTitle) => function cell(row) {
const {
fields,
} = row;
return (
<div className="form__group">
{fields.map((ip, index) => (
<div key={index} className="mb-1">
<Field
name={ip}
component={renderGroupField}
type="text"
className="form-control"
placeholder={placeholder}
isActionAvailable={index !== 0}
removeField={() => fields.remove(index)}
normalizeOnBlur={(data) => data.trim()}
/>
</div>
))}
<button
type="button"
className="btn btn-link btn-block btn-sm"
onClick={() => fields.push()}
title={buttonTitle}
>
<svg className="icon icon--24">
<use xlinkHref="#plus" />
</svg>
</button>
</div>
);
};
// Should create function outside of component to prevent component re-renders
const renderFields = renderFieldsWrapper(i18n.t('form_enter_id'), i18n.t('form_add_id'));
const renderMultiselect = (props) => {
const { input, placeholder, options } = props;
return (
<Select
{...input}
options={options}
className="basic-multi-select"
classNamePrefix="select"
onChange={(value) => input.onChange(value)}
onBlur={() => input.onBlur(input.value)}
placeholder={placeholder}
blurInputOnSelect={false}
isMulti
/>
);
};
renderMultiselect.propTypes = {
input: PropTypes.object.isRequired,
placeholder: PropTypes.string,
options: PropTypes.array,
};
let Form = (props) => {
const {
t,
handleSubmit,
reset,
change,
submitting,
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
handleClose,
processingAdding,
processingUpdating,
invalid,
tagsOptions,
initialValues,
} = props;
const services = useSelector((store) => store?.services);
const { safe_search } = initialValues;
const safeSearchServices = { ...safe_search };
delete safeSearchServices.enabled;
const [activeTabLabel, setActiveTabLabel] = useState('settings');
const handleScheduleSubmit = (values) => {
change('blocked_services_schedule', { ...values });
};
const tabs = {
settings: {
title: 'settings',
component: <div label="settings" title={props.t('main_settings')}>
<div className="form__label--bot form__label--bold">
{t('protection_section_label')}
</div>
{settingsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}>
<Field
name={setting.name}
type="checkbox"
component={CheckboxField}
placeholder={t(setting.placeholder)}
disabled={
setting.name !== 'use_global_settings'
? useGlobalSettings
: false
}
/>
</div>
))}
<div className="form__group">
<Field
name="safe_search.enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('enforce_safe_search')}
disabled={useGlobalSettings}
/>
</div>
<div className='form__group--inner'>
{Object.keys(safeSearchServices).map((searchKey) => (
<div key={searchKey}>
<Field
name={`safe_search.${searchKey}`}
type="checkbox"
component={CheckboxField}
placeholder={captitalizeWords(searchKey)}
disabled={useGlobalSettings}
/>
</div>
))}
</div>
<div className="form__label--bold form__label--top form__label--bot">
{t('log_and_stats_section_label')}
</div>
{logAndStatsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}>
<Field
name={setting.name}
type="checkbox"
component={CheckboxField}
placeholder={t(setting.placeholder)}
/>
</div>
))}
</div>,
},
block_services: {
title: 'block_services',
component: <div label="services" title={props.t('block_services')}>
<div className="form__group">
<Field
name="use_global_blocked_services"
type="checkbox"
component={renderServiceField}
placeholder={t('blocked_services_global')}
modifier="service--global"
/>
<div className="row mb-4">
<div className="col-6">
<button
type="button"
className="btn btn-secondary btn-block"
disabled={useGlobalServices}
onClick={() => (
toggleAllServices(services.allServices, change, true)
)}
>
<Trans>block_all</Trans>
</button>
</div>
<div className="col-6">
<button
type="button"
className="btn btn-secondary btn-block"
disabled={useGlobalServices}
onClick={() => (
toggleAllServices(services.allServices, change, false)
)}
>
<Trans>unblock_all</Trans>
</button>
</div>
</div>
{services.allServices.length > 0 && (
<div className="services">
{services.allServices.map((service) => (
<Field
key={service.id}
icon={service.icon_svg}
name={`blocked_services.${service.id}`}
type="checkbox"
component={renderServiceField}
placeholder={service.name}
disabled={useGlobalServices}
/>
))}
</div>
)}
</div>
</div>,
},
schedule_services: {
title: 'schedule_services',
component: (
<>
<div className="form__desc mb-4">
<Trans>schedule_services_desc_client</Trans>
</div>
<ScheduleForm
schedule={blockedServicesSchedule}
onScheduleSubmit={handleScheduleSubmit}
clientForm
/>
</>
),
},
upstream_dns: {
title: 'upstream_dns',
component: <div label="upstream" title={props.t('upstream_dns')}>
<div className="form__desc mb-3">
<Trans components={[<a href="#dns" key="0">link</a>]}>
upstream_dns_client_desc
</Trans>
</div>
<Field
id="upstreams"
name="upstreams"
component={renderTextareaField}
type="text"
className="form-control form-control--textarea mb-5"
placeholder={t('upstream_dns')}
normalizeOnBlur={trimLinesAndRemoveEmpty}
/>
<Examples />
<div className="form__label--bold mt-5 mb-3">
{t('upstream_dns_cache_configuration')}
</div>
<div className="form__group mb-2">
<Field
name="upstreams_cache_enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('enable_upstream_dns_cache')}
/>
</div>
<div className="form__group form__group--settings">
<label
htmlFor="upstreams_cache_size"
className="form__label"
>
{t('dns_cache_size')}
</label>
<Field
name="upstreams_cache_size"
type="number"
component={renderInputField}
placeholder={t('enter_cache_size')}
className="form-control"
normalize={toNumber}
min={0}
max={UINT32_RANGE.MAX}
/>
</div>
</div>,
},
};
const activeTab = tabs[activeTabLabel].component;
return (
<form onSubmit={handleSubmit}>
<div className="modal-body">
<div className="form__group mb-0">
<div className="form__group">
<Field
id="name"
name="name"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('form_client_name')}
normalizeOnBlur={(data) => data.trim()}
/>
</div>
<div className="form__group mb-4">
<div className="form__label">
<strong className="mr-3">
<Trans>tags_title</Trans>
</strong>
</div>
<div className="form__desc mt-0 mb-2">
<Trans components={[
<a target="_blank" rel="noopener noreferrer" href="https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax_ctag&from=ui&app=home"
key="0">link</a>,
]}>
tags_desc
</Trans>
</div>
<Field
name="tags"
component={renderMultiselect}
placeholder={t('form_select_tags')}
options={tagsOptions}
/>
</div>
<div className="form__group">
<div className="form__label">
<strong className="mr-3">
<Trans>client_identifier</Trans>
</strong>
</div>
<div className="form__desc mt-0">
<Trans components={[
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer"
key="0">text</a>,
]}>
client_identifier_desc
</Trans>
</div>
</div>
<div className="form__group">
<FieldArray
name="ids"
component={renderFields}
/>
</div>
</div>
<Tabs
controlClass="form"
tabs={tabs}
activeTabLabel={activeTabLabel}
setActiveTabLabel={setActiveTabLabel}
>
{activeTab}
</Tabs>
</div>
<div className="modal-footer">
<div className="btn-list">
<button
type="button"
className="btn btn-secondary btn-standard"
disabled={submitting}
onClick={() => {
reset();
handleClose();
}}
>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={
submitting
|| invalid
|| processingAdding
|| processingUpdating
}
>
<Trans>save_btn</Trans>
</button>
</div>
</div>
</form>
);
};
Form.propTypes = {
pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
change: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
handleClose: PropTypes.func.isRequired,
useGlobalSettings: PropTypes.bool,
useGlobalServices: PropTypes.bool,
blockedServicesSchedule: PropTypes.object,
t: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
tagsOptions: PropTypes.array.isRequired,
initialValues: PropTypes.object,
};
const selector = formValueSelector(FORM_NAME.CLIENT);
Form = connect((state) => {
const useGlobalSettings = selector(state, 'use_global_settings');
const useGlobalServices = selector(state, 'use_global_blocked_services');
const blockedServicesSchedule = selector(state, 'blocked_services_schedule');
return {
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
};
})(Form);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.CLIENT,
enableReinitialize: true,
validate,
}),
])(Form);

View File

@@ -0,0 +1,514 @@
import React, { useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { Field, FieldArray, reduxForm, formValueSelector, FormErrors } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import Select from 'react-select';
import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs';
import Examples from '../Dns/Upstream/Examples';
import { ScheduleForm } from '../../Filters/Services/ScheduleForm';
import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
import {
toNumber,
renderInputField,
renderGroupField,
CheckboxField,
renderServiceField,
renderTextareaField,
} from '../../../helpers/form';
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
import { CLIENT_ID_LINK, FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
import './Service.css';
import { RootState } from '../../../initialState';
const settingsCheckboxes = [
{
name: 'use_global_settings',
placeholder: 'client_global_settings',
},
{
name: 'filtering_enabled',
placeholder: 'block_domain_use_filters_and_hosts',
},
{
name: 'safebrowsing_enabled',
placeholder: 'use_adguard_browsing_sec',
},
{
name: 'parental_enabled',
placeholder: 'use_adguard_parental',
},
];
const logAndStatsCheckboxes = [
{
name: 'ignore_querylog',
placeholder: 'ignore_query_log',
},
{
name: 'ignore_statistics',
placeholder: 'ignore_statistics',
},
];
const validate = (values: any): FormErrors<any, string> => {
const errors: {
name?: string;
ids?: string[];
} = {};
const { name, ids } = values;
errors.name = validateRequiredValue(name);
if (ids && ids.length) {
const idArrayErrors: any = [];
ids.forEach((id: any, idx: any) => {
idArrayErrors[idx] = validateRequiredValue(id) || validateClientId(id);
});
if (idArrayErrors.length) {
errors.ids = idArrayErrors;
}
}
// @ts-expect-error FIXME: ts migration
return errors;
};
const renderFieldsWrapper = (placeholder: any, buttonTitle: any) =>
function cell(row: any) {
const { fields } = row;
return (
<div className="form__group">
{fields.map((ip: any, index: any) => (
<div key={index} className="mb-1">
<Field
name={ip}
component={renderGroupField}
type="text"
className="form-control"
placeholder={placeholder}
isActionAvailable={index !== 0}
removeField={() => fields.remove(index)}
normalizeOnBlur={(data: any) => data.trim()}
/>
</div>
))}
<button
type="button"
className="btn btn-link btn-block btn-sm"
onClick={() => fields.push()}
title={buttonTitle}>
<svg className="icon icon--24">
<use xlinkHref="#plus" />
</svg>
</button>
</div>
);
};
// Should create function outside of component to prevent component re-renders
const renderFields = renderFieldsWrapper(i18n.t('form_enter_id'), i18n.t('form_add_id'));
interface renderMultiselectProps {
input: {
name: string;
value: string;
checked: boolean;
onChange: (...args: unknown[]) => unknown;
onBlur: (...args: unknown[]) => unknown;
};
placeholder?: string;
options?: unknown[];
}
const renderMultiselect = (props: renderMultiselectProps) => {
const { input, placeholder, options } = props;
return (
<Select
{...input}
options={options}
className="basic-multi-select"
classNamePrefix="select"
onChange={(value: any) => input.onChange(value)}
onBlur={() => input.onBlur(input.value)}
placeholder={placeholder}
blurInputOnSelect={false}
isMulti
/>
);
};
interface FormProps {
pristine: boolean;
handleSubmit: (...args: unknown[]) => string;
reset: (...args: unknown[]) => string;
change: (...args: unknown[]) => unknown;
submitting: boolean;
handleClose: (...args: unknown[]) => unknown;
useGlobalSettings?: boolean;
useGlobalServices?: boolean;
blockedServicesSchedule?: {
time_zone: string;
};
t: (...args: unknown[]) => string;
processingAdding: boolean;
processingUpdating: boolean;
invalid: boolean;
tagsOptions: unknown[];
initialValues?: {
safe_search: any;
};
}
let Form = (props: FormProps) => {
const {
t,
handleSubmit,
reset,
change,
submitting,
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
handleClose,
processingAdding,
processingUpdating,
invalid,
tagsOptions,
initialValues,
} = props;
const services = useSelector((store: RootState) => store?.services);
const { safe_search } = initialValues;
const safeSearchServices = { ...safe_search };
delete safeSearchServices.enabled;
const [activeTabLabel, setActiveTabLabel] = useState('settings');
const handleScheduleSubmit = (values: any) => {
change('blocked_services_schedule', { ...values });
};
const tabs = {
settings: {
title: 'settings',
component: (
<div title={props.t('main_settings')}>
<div className="form__label--bot form__label--bold">{t('protection_section_label')}</div>
{settingsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}>
<Field
name={setting.name}
type="checkbox"
component={CheckboxField}
placeholder={t(setting.placeholder)}
disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
/>
</div>
))}
<div className="form__group">
<Field
name="safe_search.enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('enforce_safe_search')}
disabled={useGlobalSettings}
/>
</div>
<div className="form__group--inner">
{Object.keys(safeSearchServices).map((searchKey) => (
<div key={searchKey}>
<Field
name={`safe_search.${searchKey}`}
type="checkbox"
component={CheckboxField}
placeholder={captitalizeWords(searchKey)}
disabled={useGlobalSettings}
/>
</div>
))}
</div>
<div className="form__label--bold form__label--top form__label--bot">
{t('log_and_stats_section_label')}
</div>
{logAndStatsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}>
<Field
name={setting.name}
type="checkbox"
component={CheckboxField}
placeholder={t(setting.placeholder)}
/>
</div>
))}
</div>
),
},
block_services: {
title: 'block_services',
component: (
<div title={props.t('block_services')}>
<div className="form__group">
<Field
name="use_global_blocked_services"
type="checkbox"
component={renderServiceField}
placeholder={t('blocked_services_global')}
modifier="service--global"
/>
<div className="row mb-4">
<div className="col-6">
<button
type="button"
className="btn btn-secondary btn-block"
disabled={useGlobalServices}
onClick={() => toggleAllServices(services.allServices, change, true)}>
<Trans>block_all</Trans>
</button>
</div>
<div className="col-6">
<button
type="button"
className="btn btn-secondary btn-block"
disabled={useGlobalServices}
onClick={() => toggleAllServices(services.allServices, change, false)}>
<Trans>unblock_all</Trans>
</button>
</div>
</div>
{services.allServices.length > 0 && (
<div className="services">
{services.allServices.map((service: any) => (
<Field
key={service.id}
icon={service.icon_svg}
name={`blocked_services.${service.id}`}
type="checkbox"
component={renderServiceField}
placeholder={service.name}
disabled={useGlobalServices}
/>
))}
</div>
)}
</div>
</div>
),
},
schedule_services: {
title: 'schedule_services',
component: (
<>
<div className="form__desc mb-4">
<Trans>schedule_services_desc_client</Trans>
</div>
<ScheduleForm
schedule={blockedServicesSchedule}
onScheduleSubmit={handleScheduleSubmit}
clientForm
/>
</>
),
},
upstream_dns: {
title: 'upstream_dns',
component: (
<div title={props.t('upstream_dns')}>
<div className="form__desc mb-3">
<Trans
components={[
<a href="#dns" key="0">
link
</a>,
]}>
upstream_dns_client_desc
</Trans>
</div>
<Field
id="upstreams"
name="upstreams"
component={renderTextareaField}
type="text"
className="form-control form-control--textarea mb-5"
placeholder={t('upstream_dns')}
normalizeOnBlur={trimLinesAndRemoveEmpty}
/>
<Examples />
<div className="form__label--bold mt-5 mb-3">{t('upstream_dns_cache_configuration')}</div>
<div className="form__group mb-2">
<Field
name="upstreams_cache_enabled"
type="checkbox"
component={CheckboxField}
placeholder={t('enable_upstream_dns_cache')}
/>
</div>
<div className="form__group form__group--settings">
<label htmlFor="upstreams_cache_size" className="form__label">
{t('dns_cache_size')}
</label>
<Field
name="upstreams_cache_size"
type="number"
component={renderInputField}
placeholder={t('enter_cache_size')}
className="form-control"
normalize={toNumber}
min={0}
max={UINT32_RANGE.MAX}
/>
</div>
</div>
),
},
};
const activeTab = tabs[activeTabLabel].component;
return (
<form onSubmit={handleSubmit}>
<div className="modal-body">
<div className="form__group mb-0">
<div className="form__group">
<Field
id="name"
name="name"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('form_client_name')}
normalizeOnBlur={(data: any) => data.trim()}
/>
</div>
<div className="form__group mb-4">
<div className="form__label">
<strong className="mr-3">
<Trans>tags_title</Trans>
</strong>
</div>
<div className="form__desc mt-0 mb-2">
<Trans
components={[
<a
target="_blank"
rel="noopener noreferrer"
href="https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax_ctag&from=ui&app=home"
key="0">
link
</a>,
]}>
tags_desc
</Trans>
</div>
<Field
name="tags"
component={renderMultiselect}
placeholder={t('form_select_tags')}
options={tagsOptions}
/>
</div>
<div className="form__group">
<div className="form__label">
<strong className="mr-3">
<Trans>client_identifier</Trans>
</strong>
</div>
<div className="form__desc mt-0">
<Trans
components={[
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" key="0">
text
</a>,
]}>
client_identifier_desc
</Trans>
</div>
</div>
<div className="form__group">
<FieldArray name="ids" component={renderFields} />
</div>
</div>
<Tabs
controlClass="form"
tabs={tabs}
activeTabLabel={activeTabLabel}
setActiveTabLabel={setActiveTabLabel}>
{activeTab}
</Tabs>
</div>
<div className="modal-footer">
<div className="btn-list">
<button
type="button"
className="btn btn-secondary btn-standard"
disabled={submitting}
onClick={() => {
reset();
handleClose();
}}>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || invalid || processingAdding || processingUpdating}>
<Trans>save_btn</Trans>
</button>
</div>
</div>
</form>
);
};
const selector = formValueSelector(FORM_NAME.CLIENT);
Form = connect((state) => {
const useGlobalSettings = selector(state, 'use_global_settings');
const useGlobalServices = selector(state, 'use_global_blocked_services');
const blockedServicesSchedule = selector(state, 'blocked_services_schedule');
return {
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
};
})(Form);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.CLIENT,
enableReinitialize: true,
validate,
}),
])(Form);

View File

@@ -1,19 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { MODAL_TYPE } from '../../../helpers/constants';
import Form from './Form';
const getInitialData = ({
initial, modalType, clientId, clientName,
}) => {
const getInitialData = ({ initial, modalType, clientId, clientName }: any) => {
if (initial && initial.blocked_services) {
const { blocked_services } = initial;
const blocked = {};
blocked_services.forEach((service) => {
blocked_services.forEach((service: any) => {
blocked[service] = true;
});
@@ -34,6 +33,19 @@ const getInitialData = ({
return initial;
};
interface ModalProps {
isModalOpen: boolean;
modalType: string;
currentClientData: object;
handleSubmit: (values: any) => void;
handleClose: (...args: unknown[]) => unknown;
processingAdding: boolean;
processingUpdating: boolean;
tagsOptions: unknown[];
t: (...args: unknown[]) => string;
clientId?: string;
}
const Modal = ({
isModalOpen,
modalType,
@@ -45,7 +57,7 @@ const Modal = ({
tagsOptions,
clientId,
t,
}) => {
}: ModalProps) => {
const initialData = getInitialData({
initial: currentClientData,
modalType,
@@ -58,21 +70,18 @@ const Modal = ({
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
closeTimeoutMS={0}
isOpen={isModalOpen}
onRequestClose={handleClose}
>
onRequestClose={handleClose}>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
{modalType === MODAL_TYPE.EDIT_CLIENT ? (
<Trans>client_edit</Trans>
) : (
<Trans>client_new</Trans>
)}
{modalType === MODAL_TYPE.EDIT_CLIENT ? <Trans>client_edit</Trans> : <Trans>client_new</Trans>}
</h4>
<button type="button" className="close" onClick={handleClose}>
<span className="sr-only">Close</span>
</button>
</div>
<Form
initialValues={{ ...initialData }}
onSubmit={handleSubmit}
@@ -86,17 +95,4 @@ const Modal = ({
);
};
Modal.propTypes = {
isModalOpen: PropTypes.bool.isRequired,
modalType: PropTypes.string.isRequired,
currentClientData: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
handleClose: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
tagsOptions: PropTypes.array.isRequired,
t: PropTypes.func.isRequired,
clientId: PropTypes.string,
};
export default withTranslation()(Modal);

View File

@@ -24,9 +24,9 @@
.service {
flex-grow: 0;
flex-shrink: 0;
flex-basis: calc(99.9% * 4/12 - (30px - 30px * 4/12));
max-width: calc(99.9% * 4/12 - (30px - 30px * 4/12));
width: calc(99.9% * 4/12 - (30px - 30px * 4/12));
flex-basis: calc(99.9% * 4 / 12 - (30px - 30px * 4 / 12));
max-width: calc(99.9% * 4 / 12 - (30px - 30px * 4 / 12));
width: calc(99.9% * 4 / 12 - (30px - 30px * 4 / 12));
}
.service--global {

View File

@@ -1,34 +1,60 @@
import React, { Component, Fragment } from 'react';
import { withTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { ClientsTable } from './ClientsTable';
import AutoClients from './AutoClients';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
class Clients extends Component {
import AutoClients from './AutoClients';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import { ClientsData, DashboardData, StatsData } from '../../../initialState';
interface ClientsProps {
t: (...args: unknown[]) => string;
dashboard: DashboardData;
stats: StatsData;
clients: ClientsData;
toggleClientModal: (...args: unknown[]) => unknown;
deleteClient: (...args: unknown[]) => string;
addClient: (...args: unknown[]) => string;
updateClient: (...args: unknown[]) => string;
getClients: (...args: unknown[]) => unknown;
getStats: (...args: unknown[]) => unknown;
}
class Clients extends Component<ClientsProps> {
componentDidMount() {
this.props.getClients();
this.props.getStats();
}
render() {
const {
t,
dashboard,
stats,
clients,
addClient,
updateClient,
deleteClient,
toggleClientModal,
getStats,
} = this.props;
return (
<Fragment>
<PageTitle title={t('client_settings')} />
{(stats.processingStats || dashboard.processingClients) && <Loading />}
{!stats.processingStats && !dashboard.processingClients && (
<Fragment>
@@ -48,6 +74,7 @@ class Clients extends Component {
getStats={getStats}
supportedTags={dashboard.supportedTags}
/>
<AutoClients
autoClients={dashboard.autoClients}
normalizedTopClients={stats.normalizedTopClients}
@@ -59,17 +86,4 @@ class Clients extends Component {
}
}
Clients.propTypes = {
t: PropTypes.func.isRequired,
dashboard: PropTypes.object.isRequired,
stats: PropTypes.object.isRequired,
clients: PropTypes.object.isRequired,
toggleClientModal: PropTypes.func.isRequired,
deleteClient: PropTypes.func.isRequired,
addClient: PropTypes.func.isRequired,
updateClient: PropTypes.func.isRequired,
getClients: PropTypes.func.isRequired,
getStats: PropTypes.func.isRequired,
};
export default withTranslation()(Clients);

View File

@@ -3,7 +3,7 @@ import React, { Fragment } from 'react';
import { normalizeWhois } from '../../../helpers/helpers';
import { WHOIS_ICONS } from '../../../helpers/constants';
const getFormattedWhois = (value, t) => {
const getFormattedWhois = (value: any, t: any) => {
const whoisInfo = normalizeWhois(value);
const whoisKeys = Object.keys(whoisInfo);
@@ -29,12 +29,15 @@ const getFormattedWhois = (value, t) => {
return '';
};
const whoisCell = (t) => function cell(row) {
const { value } = row;
const whoisCell = (t: any) =>
function cell(row: any) {
const { value } = row;
return <div className="logs__row o-hidden">
<div className="logs__text logs__text--wrap">{getFormattedWhois(value, t)}</div>
</div>;
};
return (
<div className="logs__row o-hidden">
<div className="logs__text logs__text--wrap">{getFormattedWhois(value, t)}</div>
</div>
);
};
export default whoisCell;