- client: Clear input field when switching from logs page

Close #1523

Squashed commit of the following:

commit 9ae9b0e480c2ec6bd835faf4caccacd8c1fdfe9f
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Jul 3 17:56:34 2020 +0300

    Setup babel and apply everywhere nullish coalescing and optional chaining operators

commit 0966063842a79078e1d61a54c271e18c9427b2b1
Merge: 42a54f2b 21dfb5ff
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Jul 3 16:59:34 2020 +0300

    Merge branch 'master' into fix/1523

commit 42a54f2ba0d8f5f4e4c04f16abc507b5d9009035
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Jul 3 13:30:25 2020 +0300

    Center mobile tooltip buttons

commit 9cdd501a863b596f1d903f8dd3c449ffbe4c1604
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Jul 3 13:21:37 2020 +0300

    Add cross icon to clear input on click

commit 1308b93c42ffea2ffb7457c34aef96e5cdaccaf2
Merge: 265fec7c da546790
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Jun 29 19:42:04 2020 +0300

    Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home

commit 265fec7c72656b0c4738623cb470f1c14736230a
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Jun 29 19:41:37 2020 +0300

    - client: Clear input field when switching from logs page
This commit is contained in:
Artem Baskal 2020-07-03 19:17:58 +03:00
parent 21dfb5ffe8
commit 6b134469d4
22 changed files with 144 additions and 91 deletions

View File

@ -9,6 +9,8 @@ module.exports = (api) => {
'@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-class-properties',
'@babel/plugin-transform-runtime', '@babel/plugin-transform-runtime',
'@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-optional-chaining',
], ],
}; };
}; };

32
client/package-lock.json generated vendored
View File

@ -472,13 +472,21 @@
} }
}, },
"@babel/plugin-proposal-nullish-coalescing-operator": { "@babel/plugin-proposal-nullish-coalescing-operator": {
"version": "7.8.3", "version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz",
"integrity": "sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==", "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-plugin-utils": "^7.8.3", "@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
"integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
"dev": true
}
} }
}, },
"@babel/plugin-proposal-numeric-separator": { "@babel/plugin-proposal-numeric-separator": {
@ -513,13 +521,21 @@
} }
}, },
"@babel/plugin-proposal-optional-chaining": { "@babel/plugin-proposal-optional-chaining": {
"version": "7.9.0", "version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.4.tgz",
"integrity": "sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==", "integrity": "sha512-ZIhQIEeavTgouyMSdZRap4VPPHqJJ3NEs2cuHs5p0erH+iz6khB0qfgU8g7UuJkG88+fBMy23ZiU+nuHvekJeQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-plugin-utils": "^7.8.3", "@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-optional-chaining": "^7.8.0" "@babel/plugin-syntax-optional-chaining": "^7.8.0"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
"integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
"dev": true
}
} }
}, },
"@babel/plugin-proposal-unicode-property-regex": { "@babel/plugin-proposal-unicode-property-regex": {

2
client/package.json vendored
View File

@ -44,7 +44,9 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.6", "@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.9.6", "@babel/plugin-proposal-object-rest-spread": "^7.9.6",
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.9.6", "@babel/plugin-transform-runtime": "^7.9.6",
"@babel/preset-env": "^7.9.6", "@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.9.4",

View File

@ -149,7 +149,7 @@ const checkStatus = async (handleRequestSuccess, handleRequestError, attempts =
try { try {
const response = await axios.get('control/status'); const response = await axios.get('control/status');
rmTimeout(timeout); rmTimeout(timeout);
if (response && response.status === 200) { if (response?.status === 200) {
handleRequestSuccess(response); handleRequestSuccess(response);
if (response.data.running === false) { if (response.data.running === false) {
timeout = setTimeout( timeout = setTimeout(
@ -183,7 +183,7 @@ export const getUpdate = () => async (dispatch, getState) => {
}; };
const handleRequestSuccess = (response) => { const handleRequestSuccess = (response) => {
const responseVersion = response.data && response.data.version; const responseVersion = response.data?.version;
if (dnsVersion !== responseVersion) { if (dnsVersion !== responseVersion) {
dispatch(getUpdateSuccess()); dispatch(getUpdateSuccess());

View File

@ -17,7 +17,7 @@ const getInitialDataForServices = (initial) => (initial ? initial.reduce(
const Services = () => { const Services = () => {
const [t] = useTranslation(); const [t] = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const services = useSelector((store) => store && store.services); const services = useSelector((store) => store?.services);
useEffect(() => { useEffect(() => {
dispatch(getBlockedServices()); dispatch(getBlockedServices());

View File

@ -11,23 +11,20 @@ const getClientCell = ({
row, t, isDetailed, toggleBlocking, autoClients, processingRules, row, t, isDetailed, toggleBlocking, autoClients, processingRules,
}) => { }) => {
const { const {
reason, client, domain, info: { name }, reason, client, domain, info: { name, whois_info },
} = row.original; } = row.original;
const autoClient = autoClients.find((autoClient) => autoClient.name === client); const autoClient = autoClients.find((autoClient) => autoClient.name === client);
const country = autoClient && autoClient.whois_info && autoClient.whois_info.country; const source = autoClient?.source;
const city = autoClient && autoClient.whois_info && autoClient.whois_info.city;
const network = autoClient && autoClient.whois_info && autoClient.whois_info.orgname;
const source = autoClient && autoClient.source;
const id = nanoid(); const id = nanoid();
const data = { const data = {
address: client, address: client,
name, name,
country, country: whois_info?.country,
city, city: whois_info?.city,
network, network: whois_info?.orgname,
source_label: source, source_label: source,
}; };

View File

@ -57,8 +57,8 @@ const getDomainCell = (props) => {
const sourceData = getSourceData(tracker); const sourceData = getSourceData(tracker);
const knownTrackerDataObj = { const knownTrackerDataObj = {
name_table_header: tracker && tracker.name, name_table_header: tracker?.name,
category_label: tracker && captitalizeWords(tracker.category), category_label: hasTracker && captitalizeWords(tracker.category),
source_label: sourceData source_label: sourceData
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer" && <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
className="link--green">{sourceData.name}</a>, className="link--green">{sourceData.name}</a>,

View File

@ -36,8 +36,7 @@ const getResponseCell = (row, filtering, t, isDetailed) => {
const { filters, whitelistFilters } = filtering; const { filters, whitelistFilters } = filtering;
const formattedElapsedMs = formatElapsedMs(elapsedMs, t); const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
const statusLabel = t((FILTERED_STATUS_TO_META_MAP[reason] const statusLabel = t(FILTERED_STATUS_TO_META_MAP[reason]?.label || reason);
&& FILTERED_STATUS_TO_META_MAP[reason].label) || reason);
const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>; const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>;
const filter = getFilterName(filters, whitelistFilters, filterId, t); const filter = getFilterName(filters, whitelistFilters, filterId, t);

View File

@ -1,10 +1,18 @@
import React from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form'; import { Field, reduxForm } from 'redux-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { DEBOUNCE_FILTER_TIMEOUT, FORM_NAME, RESPONSE_FILTER } from '../../../helpers/constants'; import { useDispatch } from 'react-redux';
import classNames from 'classnames';
import {
DEBOUNCE_FILTER_TIMEOUT,
DEFAULT_LOGS_FILTER,
FORM_NAME,
RESPONSE_FILTER,
} from '../../../helpers/constants';
import Tooltip from '../../ui/Tooltip'; import Tooltip from '../../ui/Tooltip';
import { setLogsFilter } from '../../../actions/queryLogs';
const renderFilterField = ({ const renderFilterField = ({
input, input,
@ -16,8 +24,9 @@ const renderFilterField = ({
autoComplete, autoComplete,
tooltip, tooltip,
meta: { touched, error }, meta: { touched, error },
onClearInputClick,
}) => <> }) => <>
<div className="input-group-search"> <div className="input-group-search input-group-search__icon--magnifier">
<svg className="icons icon--small icon--gray"> <svg className="icons icon--small icon--gray">
<use xlinkHref="#magnifier" /> <use xlinkHref="#magnifier" />
</svg> </svg>
@ -31,7 +40,13 @@ const renderFilterField = ({
disabled={disabled} disabled={disabled}
autoComplete={autoComplete} autoComplete={autoComplete}
aria-label={placeholder} /> aria-label={placeholder} />
<span className="logs__notice"> <div
className={classNames('input-group-search input-group-search__icon--cross', { invisible: input.value.length < 1 })}>
<svg className="icons icon--smallest icon--gray" onClick={onClearInputClick}>
<use xlinkHref="#cross" />
</svg>
</div>
<span className="input-group-search input-group-search__icon--tooltip">
<Tooltip text={tooltip} type='tooltip-custom--logs' /> <Tooltip text={tooltip} type='tooltip-custom--logs' />
</span> </span>
{!disabled {!disabled
@ -42,6 +57,7 @@ const renderFilterField = ({
renderFilterField.propTypes = { renderFilterField.propTypes = {
input: PropTypes.object.isRequired, input: PropTypes.object.isRequired,
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
onClearInputClick: PropTypes.func.isRequired,
className: PropTypes.string, className: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
@ -59,13 +75,29 @@ const Form = (props) => {
className = '', className = '',
responseStatusClass, responseStatusClass,
submit, submit,
reset,
setIsLoading,
} = props; } = props;
const [t] = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch();
const debouncedSubmit = debounce(submit, DEBOUNCE_FILTER_TIMEOUT); const debouncedSubmit = debounce(submit, DEBOUNCE_FILTER_TIMEOUT);
const zeroDelaySubmit = () => setTimeout(submit, 0); const zeroDelaySubmit = () => setTimeout(submit, 0);
const clearInput = async () => {
await dispatch(setLogsFilter(DEFAULT_LOGS_FILTER));
await reset();
};
const onInputClear = async () => {
setIsLoading(true);
await clearInput();
setIsLoading(false);
};
useEffect(() => clearInput, []);
return ( return (
<form className="d-flex flex-wrap form-control--container" <form className="d-flex flex-wrap form-control--container"
onSubmit={(e) => { onSubmit={(e) => {
@ -79,16 +111,17 @@ const Form = (props) => {
name="search" name="search"
component={renderFilterField} component={renderFilterField}
type="text" type="text"
className={`form-control--search form-control--transparent ${className}`} className={classNames('form-control--search form-control--transparent', className)}
placeholder={t('domain_or_client')} placeholder={t('domain_or_client')}
tooltip={t('query_log_strict_search')} tooltip={t('query_log_strict_search')}
onChange={debouncedSubmit} onChange={debouncedSubmit}
onClearInputClick={onInputClear}
/> />
<div className="field__select"> <div className="field__select">
<Field <Field
name="response_status" name="response_status"
component="select" component="select"
className={`form-control custom-select custom-select--logs custom-select__arrow--left ml-small form-control--transparent ${responseStatusClass}`} className={classNames('form-control custom-select custom-select--logs custom-select__arrow--left ml-small form-control--transparent', responseStatusClass)}
onChange={zeroDelaySubmit} onChange={zeroDelaySubmit}
> >
{Object.values(RESPONSE_FILTER) {Object.values(RESPONSE_FILTER)
@ -107,6 +140,8 @@ Form.propTypes = {
className: PropTypes.string, className: PropTypes.string,
responseStatusClass: PropTypes.string, responseStatusClass: PropTypes.string,
submit: PropTypes.func.isRequired, submit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
setIsLoading: PropTypes.func.isRequired,
}; };
export default reduxForm({ export default reduxForm({

View File

@ -33,6 +33,7 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => {
responseStatusClass="d-sm-block" responseStatusClass="d-sm-block"
initialValues={filter} initialValues={filter}
onSubmit={onSubmit} onSubmit={onSubmit}
setIsLoading={setIsLoading}
/> />
</div> </div>
); );

View File

@ -199,17 +199,6 @@
position: relative; position: relative;
} }
.logs__notice {
position: relative;
z-index: 1;
top: 0.5rem;
right: 2rem;
margin-top: 0.1875rem;
font-size: 0.75rem;
text-align: left;
color: var(--gray-a5);
}
.logs__whois { .logs__whois {
display: inline; display: inline;
font-size: 12px; font-size: 12px;
@ -485,10 +474,22 @@
.input-group-search { .input-group-search {
background-color: transparent; background-color: transparent;
position: relative; position: relative;
left: 2rem;
top: 0.4rem;
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
top: 0.4rem;
}
.input-group-search__icon--magnifier {
left: 2rem;
}
.input-group-search__icon--cross {
left: -4.5rem;
cursor: pointer;
}
.input-group-search__icon--tooltip {
left: -4rem;
} }
.form-control--container { .form-control--container {

View File

@ -180,6 +180,7 @@ const Table = (props) => {
minWidth: 123, minWidth: 123,
maxHeight: 60, maxHeight: 60,
headerClassName: 'logs__text', headerClassName: 'logs__text',
className: 'pb-0',
}, },
]; ];
@ -279,20 +280,15 @@ const Table = (props) => {
const hasTracker = !!tracker; const hasTracker = !!tracker;
const autoClient = autoClients.find( const autoClient = autoClients
(autoClient) => autoClient.name === client, .find((autoClient) => autoClient.name === client);
);
const country = autoClient && autoClient.whois_info const { whois_info } = info;
&& autoClient.whois_info.country; const country = whois_info?.country;
const city = whois_info?.city;
const network = whois_info?.orgname;
const network = autoClient && autoClient.whois_info const source = autoClient?.source;
&& autoClient.whois_info.orgname;
const city = autoClient && autoClient.whois_info
&& autoClient.whois_info.city;
const source = autoClient && autoClient.source;
const formattedElapsedMs = formatElapsedMs(elapsedMs, t); const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
const isFiltered = checkFiltered(reason); const isFiltered = checkFiltered(reason);
@ -302,8 +298,7 @@ const Table = (props) => {
toggleBlocking(buttonType, domain); toggleBlocking(buttonType, domain);
}; };
const status = t((FILTERED_STATUS_TO_META_MAP[reason] const status = t(FILTERED_STATUS_TO_META_MAP[reason]?.label || reason);
&& FILTERED_STATUS_TO_META_MAP[reason].label) || reason);
const statusBlocked = <div className="bg--danger">{status}</div>; const statusBlocked = <div className="bg--danger">{status}</div>;
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || ''; const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
@ -318,7 +313,7 @@ const Table = (props) => {
type_table_header: type, type_table_header: type,
protocol, protocol,
known_tracker: hasTracker && 'title', known_tracker: hasTracker && 'title',
table_name: hasTracker && tracker.name, table_name: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category), category_label: hasTracker && captitalizeWords(tracker.category),
tracker_source: hasTracker && sourceData tracker_source: hasTracker && sourceData
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer" && <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
@ -326,17 +321,17 @@ const Table = (props) => {
response_details: 'title', response_details: 'title',
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
response_table_header: response && response.join('\n'), response_table_header: response?.join('\n'),
client_details: 'title', client_details: 'title',
ip_address: client, ip_address: client,
name: info && info.name, name: info?.name,
country, country,
city, city,
network, network,
source_label: source, source_label: source,
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false, validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
[buttonType]: <div onClick={onToggleBlock} [buttonType]: <div onClick={onToggleBlock}
className="title--border bg--danger">{t(buttonType)}</div>, className="title--border bg--danger text-center">{t(buttonType)}</div>,
}; };
const detailedDataBlocked = { const detailedDataBlocked = {
@ -347,7 +342,7 @@ const Table = (props) => {
type_table_header: type, type_table_header: type,
protocol, protocol,
known_tracker: 'title', known_tracker: 'title',
table_name: hasTracker && tracker.name, table_name: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category), category_label: hasTracker && captitalizeWords(tracker.category),
source_label: hasTracker && sourceData source_label: hasTracker && sourceData
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer" && <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
@ -355,9 +350,9 @@ const Table = (props) => {
response_details: 'title', response_details: 'title',
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
response_table_header: response && response.join('\n'), response_table_header: response?.join('\n'),
[buttonType]: <div onClick={onToggleBlock} [buttonType]: <div onClick={onToggleBlock}
className="title--border">{t(buttonType)}</div>, className="title--border text-center">{t(buttonType)}</div>,
}; };
const detailedDataCurrent = isFiltered ? detailedDataBlocked : detailedData; const detailedDataCurrent = isFiltered ? detailedDataBlocked : detailedData;

View File

@ -12,12 +12,12 @@ import Loading from '../ui/Loading';
import Filters from './Filters'; import Filters from './Filters';
import Table from './Table'; import Table from './Table';
import Disabled from './Disabled'; import Disabled from './Disabled';
import './Logs.css';
import { getFilteringStatus } from '../../actions/filtering'; import { getFilteringStatus } from '../../actions/filtering';
import { getClients } from '../../actions'; import { getClients } from '../../actions';
import { getDnsConfig } from '../../actions/dnsConfig'; import { getDnsConfig } from '../../actions/dnsConfig';
import { getLogsConfig } from '../../actions/queryLogs'; import { getLogsConfig } from '../../actions/queryLogs';
import { addSuccessToast } from '../../actions/toasts'; import { addSuccessToast } from '../../actions/toasts';
import './Logs.css';
const INITIAL_REQUEST = true; const INITIAL_REQUEST = true;
const INITIAL_REQUEST_DATA = ['', TABLE_FIRST_PAGE, INITIAL_REQUEST]; const INITIAL_REQUEST_DATA = ['', TABLE_FIRST_PAGE, INITIAL_REQUEST];

View File

@ -32,7 +32,7 @@ class Dhcp extends Component {
const { const {
config, check, processingDhcp, processingConfig, config, check, processingDhcp, processingConfig,
} = this.props.dhcp; } = this.props.dhcp;
const otherDhcpFound = check && check.otherServer const otherDhcpFound = check?.otherServer
&& check.otherServer.found === DHCP_STATUS_RESPONSE.YES; && check.otherServer.found === DHCP_STATUS_RESPONSE.YES;
const filledConfig = Object.keys(config).every((key) => { const filledConfig = Object.keys(config).every((key) => {
if (key === 'enabled' || key === 'icmp_timeout_msec') { if (key === 'enabled' || key === 'icmp_timeout_msec') {

View File

@ -9,7 +9,7 @@ import './Toast.css';
const Toasts = (props) => ( const Toasts = (props) => (
<TransitionGroup className="toasts"> <TransitionGroup className="toasts">
{props.toasts.notices && props.toasts.notices.map((toast) => { {props.toasts.notices?.map((toast) => {
const { id } = toast; const { id } = toast;
return ( return (
<CSSTransition <CSSTransition

View File

@ -120,7 +120,7 @@ const renderDnsPrivacyList = ({ title, list }) => <div className="tab__paragraph
<strong><Trans>{title}</Trans></strong> <strong><Trans>{title}</Trans></strong>
<ul>{list.map(({ label, components }) => <li key={label}> <ul>{list.map(({ label, components }) => <li key={label}>
<Trans <Trans
components={components && components.map((props) => { components={components?.map((props) => {
if (React.isValidElement(props)) { if (React.isValidElement(props)) {
return props; return props;
} }
@ -198,7 +198,7 @@ const getTabs = ({
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
getTitle: () => <div label="dns_privacy" title={t('dns_privacy')}> getTitle: () => <div label="dns_privacy" title={t('dns_privacy')}>
<div className="tab__text"> <div className="tab__text">
{tlsAddress && tlsAddress.length > 0 && ( {tlsAddress?.length > 0 && (
<div className="tab__paragraph"> <div className="tab__paragraph">
<Trans <Trans
values={{ address: tlsAddress[0] }} values={{ address: tlsAddress[0] }}
@ -211,7 +211,7 @@ const getTabs = ({
</Trans> </Trans>
</div> </div>
)} )}
{httpsAddress && httpsAddress.length > 0 && ( {httpsAddress?.length > 0 && (
<div className="tab__paragraph"> <div className="tab__paragraph">
<Trans <Trans
values={{ address: httpsAddress[0] }} values={{ address: httpsAddress[0] }}

View File

@ -9,6 +9,11 @@
height: 1.5rem; height: 1.5rem;
} }
.icon--smallest {
width: 1.2rem;
height: 1.2rem;
}
.icon--gray { .icon--gray {
color: var(--gray-a5); color: var(--gray-a5);
} }

View File

@ -173,7 +173,7 @@ export const renderSelectField = ({
</label> </label>
{!disabled {!disabled
&& touched && touched
&& (error && <span className="form__message form__message--error">{error}</span>)} && error && <span className="form__message form__message--error">{error}</span>}
</Fragment>; </Fragment>;
renderSelectField.propTypes = { renderSelectField.propTypes = {
@ -232,7 +232,7 @@ renderServiceField.propTypes = {
// https://redux-form.com/6.6.3/examples/fieldlevelvalidation/ // https://redux-form.com/6.6.3/examples/fieldlevelvalidation/
export const required = (value) => { export const required = (value) => {
const formattedValue = typeof value === 'string' ? value.trim() : value; const formattedValue = typeof value === 'string' ? value.trim() : value;
if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) { if (formattedValue || formattedValue === 0 || formattedValue?.length !== 0) {
return undefined; return undefined;
} }
return <Trans>form_error_required</Trans>; return <Trans>form_error_required</Trans>;

View File

@ -1,4 +1,4 @@
import React, { Fragment } from 'react'; import React from 'react';
import { normalizeWhois } from './helpers'; import { normalizeWhois } from './helpers';
import { WHOIS_ICONS } from './constants'; import { WHOIS_ICONS } from './constants';
@ -11,12 +11,12 @@ const getFormattedWhois = (whois, t) => {
return ( return (
<span className="logs__whois text-muted" key={key} title={t(key)}> <span className="logs__whois text-muted" key={key} title={t(key)}>
{icon && ( {icon && (
<Fragment> <>
<svg className="logs__whois-icon icons"> <svg className="logs__whois-icon icons">
<use xlinkHref={`#${icon}`} /> <use xlinkHref={`#${icon}`} />
</svg> </svg>
&nbsp; &nbsp;
</Fragment> </>
)}{whoisInfo[key]} )}{whoisInfo[key]}
</span> </span>
); );

View File

@ -142,7 +142,7 @@ export const addClientInfo = (data, clients, param) => (
const info = clients.find((item) => item[clientIp]) || ''; const info = clients.find((item) => item[clientIp]) || '';
return { return {
...row, ...row,
info: (info && info[clientIp]) || '', info: info?.[clientIp] ?? '',
}; };
}) })
); );
@ -342,7 +342,7 @@ export const normalizeTopClients = (topClients) => topClients.reduce(
export const getClientInfo = (clients, ip) => { export const getClientInfo = (clients, ip) => {
const client = clients const client = clients
.find((item) => item.ip_addrs && item.ip_addrs.find((clientIp) => clientIp === ip)); .find((item) => item.ip_addrs?.find((clientIp) => clientIp === ip));
if (!client) { if (!client) {
return ''; return '';
@ -403,7 +403,7 @@ export const secondsToMilliseconds = (seconds) => {
return seconds; return seconds;
}; };
export const normalizeRulesTextarea = (text) => text && text.replace(/^\n/g, '') export const normalizeRulesTextarea = (text) => text?.replace(/^\n/g, '')
.replace(/\n\s*\n/g, '\n'); .replace(/\n\s*\n/g, '\n');
export const isVersionGreater = (currentVersion, previousVersion) => ( export const isVersionGreater = (currentVersion, previousVersion) => (
@ -415,7 +415,7 @@ export const normalizeWhois = (whois) => {
const { const {
city, country, ...values city, country, ...values
} = whois; } = whois;
let location = (country && country) || ''; let location = country || '';
if (city && location) { if (city && location) {
location = `${location}, ${city}`; location = `${location}, ${city}`;
@ -483,7 +483,7 @@ export const checkParental = (reason) => reason === FILTERED_STATUS.FILTERED_PAR
export const checkBlockedService = (reason) => reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE; export const checkBlockedService = (reason) => reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
export const getCurrentFilter = (url, filters) => { export const getCurrentFilter = (url, filters) => {
const filter = filters && filters.find((item) => url === item.url); const filter = filters?.find((item) => url === item.url);
if (filter) { if (filter) {
const { enabled, name, url } = filter; const { enabled, name, url } = filter;

View File

@ -29,9 +29,9 @@ const renderInterfaces = ((interfaces) => (
flags, flags,
} = option; } = option;
if (option && ip_addresses && ip_addresses.length > 0) { if (option && ip_addresses?.length > 0) {
const ip = getInterfaceIp(option); const ip = getInterfaceIp(option);
const isDown = flags && flags.includes('down'); const isDown = flags?.includes('down');
if (isDown) { if (isDown) {
return ( return (

View File

@ -14,9 +14,9 @@ const access = handleActions(
} = payload; } = payload;
const newState = { const newState = {
...state, ...state,
allowed_clients: (allowed_clients && allowed_clients.join('\n')) || '', allowed_clients: allowed_clients?.join('\n') || '',
disallowed_clients: (disallowed_clients && disallowed_clients.join('\n')) || '', disallowed_clients: disallowed_clients?.join('\n') || '',
blocked_hosts: (blocked_hosts && blocked_hosts.join('\n')) || '', blocked_hosts: blocked_hosts?.join('\n') || '',
processing: false, processing: false,
}; };
return newState; return newState;
@ -42,9 +42,9 @@ const access = handleActions(
} = payload; } = payload;
const newState = { const newState = {
...state, ...state,
allowed_clients: (allowed_clients && allowed_clients.join('\n')) || '', allowed_clients: allowed_clients?.join('\n') || '',
disallowed_clients: (disallowed_clients && disallowed_clients.join('\n')) || '', disallowed_clients: disallowed_clients?.join('\n') || '',
blocked_hosts: (blocked_hosts && blocked_hosts.join('\n')) || '', blocked_hosts: blocked_hosts?.join('\n') || '',
processingSet: false, processingSet: false,
}; };
return newState; return newState;