+client: "Drill down" to activity reports
Close #1625 Squashed commit of the following: commit a01f12c4e5831c43dbe3ae8a80f4db12077dbb2a Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 15:50:15 2020 +0300 minor commit b8ceb17a3b12e47de81af85fa30c2961a4a42fab Merge: 702c55ed fecf5494 Author: Andrey Meshkov <am@adguard.com> Date: Mon Jul 13 15:32:44 2020 +0300 Merge branch 'feature/1625' of ssh://bit.adguard.com:7999/dns/adguard-home into feature/1625 commit 702c55edc1ba2ab330eda8189498dfff33c92f5f Author: Andrey Meshkov <am@adguard.com> Date: Mon Jul 13 15:32:41 2020 +0300 fix makefile when there's no gopath commit fecf5494b8c1719cb70044f336fe99c341802d25 Merge: d4c811f98a417604Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 15:30:21 2020 +0300 Merge branch 'master' into feature/1625 commit d4c811f9630dee448012434e2f50f34ab8b8b899 Merge: b0a037daa33164bfAuthor: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 12:35:16 2020 +0300 Merge branch 'master' into feature/1625 commit b0a037daf48913fd8a4cda16d520835630072520 Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 12:34:42 2020 +0300 Simplify sync logs action creators commit eeeb620ae100a554f59783fc2a14fad525ce1a82 Author: ArtemBaskal <a.baskal@adguard.com> Date: Mon Jul 13 11:17:08 2020 +0300 Review changes commit 4cbc59eec5c794df18d6cb9b33f39091ce7cfde9 Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Jul 10 15:23:37 2020 +0300 Update tracker tooltip class commit 0a705301d4726af1c8f7f7a5776b11d338ab1d54 Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Jul 10 13:46:10 2020 +0300 Replace depricated addListener commit 2ac0843239853da1725d2e038b5e4cbaef253732 Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Jul 10 13:39:45 2020 +0300 Validate response_status url param commit 2178039ebbd0cbe2c0048cb5ab7ad7c7e7571bd1 Author: ArtemBaskal <a.baskal@adguard.com> Date: Fri Jul 10 12:58:18 2020 +0300 Fix setting empty search value, use strict search on drill down, extract refreshFilteredLogs action commit 4b11c6a34049bd133077bad035d267f87cdec141 Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Jul 9 19:41:48 2020 +0300 Normalize input search commit 3fded3575b21bdd017723f5e487c268074599e4f Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Jul 9 18:20:05 2020 +0300 Optimize search commit 9073e032e4aadcdef9d826f16a10c300ee46b30e Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Jul 9 14:28:41 2020 +0300 Update url string params commit a18cffc8bfac83103fb78ffae2f786f89aea8ba1 Author: ArtemBaskal <a.baskal@adguard.com> Date: Thu Jul 9 12:55:50 2020 +0300 Fix reset search commit 33f769aed56369aacedd29ffd52b527b527d4a59 Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jul 8 19:13:21 2020 +0300 WIP: Add permlinks commit 4422641cf5cff06c8485ea23d58e5d42f7cca5cd Author: ArtemBaskal <a.baskal@adguard.com> Date: Wed Jul 8 14:42:28 2020 +0300 Refactor Counters, add response_status links to query log commit e8bb0b70ca55f31ef3fcdda13dcaad6f5d8479b5 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jul 7 19:33:04 2020 +0300 Delete unnecessary file commit b20816e9dad79866e3ec04d3093c972967b3b226 Merge: 6281084e d2c3af5c Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jul 7 19:30:44 2020 +0300 Resolve conflict commit d2c3af5cf227d76f876d6d94ca016d4b242b2515 Author: ArtemBaskal <a.baskal@adguard.com> Date: Tue Jul 7 17:14:51 2020 +0300 + client: Add git hooks ... and 5 more commits
This commit is contained in:
@@ -36,7 +36,7 @@ import i18n from '../../i18n';
|
||||
import Loading from '../ui/Loading';
|
||||
import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS } from '../../helpers/constants';
|
||||
import Services from '../Filters/Services';
|
||||
import { setHtmlLangAttr } from '../../helpers/helpers';
|
||||
import { getLogsUrlParams, setHtmlLangAttr } from '../../helpers/helpers';
|
||||
|
||||
class App extends Component {
|
||||
componentDidMount() {
|
||||
@@ -111,7 +111,9 @@ class App extends Component {
|
||||
{!dashboard.processing && dashboard.isCoreRunning && (
|
||||
<>
|
||||
<Route path={MENU_URLS.root} exact component={Dashboard} />
|
||||
<Route path={MENU_URLS.logs} component={Logs} />
|
||||
<Route
|
||||
path={[`${MENU_URLS.logs}${getLogsUrlParams(':search?', ':response_status?')}`, MENU_URLS.logs]}
|
||||
component={Logs} />
|
||||
<Route path={MENU_URLS.guide} component={SetupGuide} />
|
||||
<Route path={SETTINGS_URLS.settings} component={Settings} />
|
||||
<Route path={SETTINGS_URLS.dns} component={Dns} />
|
||||
|
||||
@@ -14,7 +14,11 @@ const CountCell = (totalBlocked) => function cell(row) {
|
||||
const { value } = row;
|
||||
const percent = getPercent(totalBlocked, value);
|
||||
|
||||
return <Cell value={value} percent={percent} color={STATUS_COLORS.red} />;
|
||||
return <Cell value={value}
|
||||
percent={percent}
|
||||
color={STATUS_COLORS.red}
|
||||
search={row.original.domain}
|
||||
/>;
|
||||
};
|
||||
|
||||
const BlockedDomains = ({
|
||||
|
||||
@@ -25,7 +25,7 @@ const countCell = (dnsQueries) => function cell(row) {
|
||||
const percent = getPercent(dnsQueries, value);
|
||||
const percentColor = getClientsPercentColor(percent);
|
||||
|
||||
return <Cell value={value} percent={percent} color={percentColor} />;
|
||||
return <Cell value={value} percent={percent} color={percentColor} search={row.original.ip} />;
|
||||
};
|
||||
|
||||
const renderBlockingButton = (ipMatchListStatus, ip, handleClick, processing) => {
|
||||
|
||||
@@ -1,31 +1,80 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import propTypes from 'prop-types';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import round from 'lodash/round';
|
||||
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import Card from '../ui/Card';
|
||||
import Tooltip from '../ui/Tooltip';
|
||||
import IconTooltip from '../ui/IconTooltip';
|
||||
import { formatNumber } from '../../helpers/helpers';
|
||||
import LogsSearchLink from '../ui/LogsSearchLink';
|
||||
import { RESPONSE_FILTER } from '../../helpers/constants';
|
||||
|
||||
const tooltipType = 'tooltip-custom--narrow';
|
||||
const Row = ({
|
||||
label, count, response_status, tooltipTitle, translationComponents,
|
||||
}) => {
|
||||
const content = response_status
|
||||
? <LogsSearchLink response_status={response_status}>{formatNumber(count)}</LogsSearchLink>
|
||||
: count;
|
||||
|
||||
const Counters = (props) => {
|
||||
return <tr key={label}>
|
||||
<td>
|
||||
<Trans components={translationComponents}>{label}</Trans>
|
||||
<IconTooltip text={tooltipTitle} type="tooltip-custom--narrow" />
|
||||
</td>
|
||||
<td className="text-right"><strong>{content}</strong></td>
|
||||
</tr>;
|
||||
};
|
||||
|
||||
const Counters = ({ refreshButton, subtitle }) => {
|
||||
const {
|
||||
t,
|
||||
interval,
|
||||
refreshButton,
|
||||
subtitle,
|
||||
dnsQueries,
|
||||
blockedFiltering,
|
||||
replacedSafebrowsing,
|
||||
replacedParental,
|
||||
replacedSafesearch,
|
||||
numDnsQueries,
|
||||
numBlockedFiltering,
|
||||
numReplacedSafebrowsing,
|
||||
numReplacedParental,
|
||||
numReplacedSafesearch,
|
||||
avgProcessingTime,
|
||||
} = props;
|
||||
} = useSelector((state) => state.stats, shallowEqual);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tooltipTitle = interval === 1
|
||||
? t('number_of_dns_query_24_hours')
|
||||
: t('number_of_dns_query_days', { count: interval });
|
||||
const rows = [
|
||||
{
|
||||
label: 'dns_query',
|
||||
count: numDnsQueries,
|
||||
tooltipTitle: interval === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: interval }),
|
||||
response_status: RESPONSE_FILTER.ALL.query,
|
||||
},
|
||||
{
|
||||
label: 'blocked_by',
|
||||
count: numBlockedFiltering,
|
||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours',
|
||||
response_status: RESPONSE_FILTER.BLOCKED.query,
|
||||
translationComponents: [<a href="#filters" key="0">link</a>],
|
||||
},
|
||||
{
|
||||
label: 'stats_malware_phishing',
|
||||
count: numReplacedSafebrowsing,
|
||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec',
|
||||
response_status: RESPONSE_FILTER.BLOCKED_THREATS.query,
|
||||
},
|
||||
{
|
||||
label: 'stats_adult',
|
||||
count: numReplacedParental,
|
||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult',
|
||||
response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.query,
|
||||
},
|
||||
{
|
||||
label: 'enforced_save_search',
|
||||
count: numReplacedSafesearch,
|
||||
tooltipTitle: 'number_of_dns_query_to_safe_search',
|
||||
response_status: RESPONSE_FILTER.SAFE_SEARCH.query,
|
||||
},
|
||||
{
|
||||
label: 'average_processing_time',
|
||||
count: avgProcessingTime ? `${round(avgProcessingTime)} ms` : 0,
|
||||
tooltipTitle: 'average_processing_time_hint',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -35,104 +84,23 @@ const Counters = (props) => {
|
||||
refresh={refreshButton}
|
||||
>
|
||||
<table className="table card-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>dns_query</Trans>
|
||||
<Tooltip text={tooltipTitle} type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(dnsQueries)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans components={[<a href="#filters" key="0">link</a>]}>
|
||||
blocked_by
|
||||
</Trans>
|
||||
<Tooltip
|
||||
text={t('number_of_dns_query_blocked_24_hours')}
|
||||
type={tooltipType}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(blockedFiltering)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>stats_malware_phishing</Trans>
|
||||
<Tooltip
|
||||
text={t('number_of_dns_query_blocked_24_hours_by_sec')}
|
||||
type={tooltipType}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(replacedSafebrowsing)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>stats_adult</Trans>
|
||||
<Tooltip
|
||||
text={t('number_of_dns_query_blocked_24_hours_adult')}
|
||||
type={tooltipType}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(replacedParental)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>enforced_save_search</Trans>
|
||||
<Tooltip
|
||||
text={t('number_of_dns_query_to_safe_search')}
|
||||
type={tooltipType}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{formatNumber(replacedSafesearch)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>average_processing_time</Trans>
|
||||
<Tooltip text={t('average_processing_time_hint')} type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
{avgProcessingTime ? `${round(avgProcessingTime)} ms` : 0}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>{rows.map(Row)}</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
Counters.propTypes = {
|
||||
dnsQueries: PropTypes.number.isRequired,
|
||||
blockedFiltering: PropTypes.number.isRequired,
|
||||
replacedSafebrowsing: PropTypes.number.isRequired,
|
||||
replacedParental: PropTypes.number.isRequired,
|
||||
replacedSafesearch: PropTypes.number.isRequired,
|
||||
avgProcessingTime: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
interval: PropTypes.number.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
Row.propTypes = {
|
||||
label: propTypes.string.isRequired,
|
||||
count: propTypes.string.isRequired,
|
||||
response_status: propTypes.string,
|
||||
tooltipTitle: propTypes.string.isRequired,
|
||||
translationComponents: propTypes.arrayOf(propTypes.element),
|
||||
};
|
||||
|
||||
export default withTranslation()(Counters);
|
||||
Counters.propTypes = {
|
||||
refreshButton: propTypes.node.isRequired,
|
||||
subtitle: propTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Counters;
|
||||
|
||||
@@ -13,7 +13,8 @@ import { getPercent } from '../../helpers/helpers';
|
||||
const getQueriedPercentColor = (percent) => {
|
||||
if (percent > 10) {
|
||||
return STATUS_COLORS.red;
|
||||
} if (percent > 5) {
|
||||
}
|
||||
if (percent > 5) {
|
||||
return STATUS_COLORS.yellow;
|
||||
}
|
||||
return STATUS_COLORS.green;
|
||||
@@ -24,7 +25,8 @@ const countCell = (dnsQueries) => function cell(row) {
|
||||
const percent = getPercent(dnsQueries, value);
|
||||
const percentColor = getQueriedPercentColor(percent);
|
||||
|
||||
return <Cell value={value} percent={percent} color={percentColor} />;
|
||||
return <Cell value={value} percent={percent} color={percentColor}
|
||||
search={row.original.domain} />;
|
||||
};
|
||||
|
||||
const QueriedDomains = ({
|
||||
|
||||
@@ -111,13 +111,6 @@ class Dashboard extends Component {
|
||||
<div className="col-lg-6">
|
||||
<Counters
|
||||
subtitle={subtitle}
|
||||
interval={stats.interval}
|
||||
dnsQueries={stats.numDnsQueries}
|
||||
blockedFiltering={stats.numBlockedFiltering}
|
||||
replacedSafebrowsing={stats.numReplacedSafebrowsing}
|
||||
replacedParental={stats.numReplacedParental}
|
||||
replacedSafesearch={stats.numReplacedSafesearch}
|
||||
avgProcessingTime={stats.avgProcessingTime}
|
||||
refreshButton={refreshButton}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import enhanceWithClickOutside from 'react-click-outside';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
|
||||
import { SETTINGS_URLS, FILTERS_URLS, MENU_URLS } from '../../helpers/constants';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{
|
||||
route: MENU_URLS.root, exact: true, icon: 'dashboard', text: 'dashboard', order: 0,
|
||||
route: MENU_URLS.root,
|
||||
exact: true,
|
||||
icon: 'dashboard',
|
||||
text: 'dashboard',
|
||||
order: 0,
|
||||
},
|
||||
|
||||
// Settings dropdown should have visual order 1
|
||||
@@ -18,27 +21,63 @@ const MENU_ITEMS = [
|
||||
// Filters dropdown should have visual order 2
|
||||
|
||||
{
|
||||
route: MENU_URLS.logs, icon: 'log', text: 'query_log', order: 3,
|
||||
route: MENU_URLS.logs,
|
||||
icon: 'log',
|
||||
text: 'query_log',
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
route: MENU_URLS.guide, icon: 'setup', text: 'setup_guide', order: 4,
|
||||
route: MENU_URLS.guide,
|
||||
icon: 'setup',
|
||||
text: 'setup_guide',
|
||||
order: 4,
|
||||
},
|
||||
];
|
||||
|
||||
const SETTINGS_ITEMS = [
|
||||
{ route: SETTINGS_URLS.settings, text: 'general_settings' },
|
||||
{ route: SETTINGS_URLS.dns, text: 'dns_settings' },
|
||||
{ route: SETTINGS_URLS.encryption, text: 'encryption_settings' },
|
||||
{ route: SETTINGS_URLS.clients, text: 'client_settings' },
|
||||
{ route: SETTINGS_URLS.dhcp, text: 'dhcp_settings' },
|
||||
{
|
||||
route: SETTINGS_URLS.settings,
|
||||
text: 'general_settings',
|
||||
},
|
||||
{
|
||||
route: SETTINGS_URLS.dns,
|
||||
text: 'dns_settings',
|
||||
},
|
||||
{
|
||||
route: SETTINGS_URLS.encryption,
|
||||
text: 'encryption_settings',
|
||||
},
|
||||
{
|
||||
route: SETTINGS_URLS.clients,
|
||||
text: 'client_settings',
|
||||
},
|
||||
{
|
||||
route: SETTINGS_URLS.dhcp,
|
||||
text: 'dhcp_settings',
|
||||
},
|
||||
];
|
||||
|
||||
const FILTERS_ITEMS = [
|
||||
{ route: FILTERS_URLS.dns_blocklists, text: 'dns_blocklists' },
|
||||
{ route: FILTERS_URLS.dns_allowlists, text: 'dns_allowlists' },
|
||||
{ route: FILTERS_URLS.dns_rewrites, text: 'dns_rewrites' },
|
||||
{ route: FILTERS_URLS.blocked_services, text: 'blocked_services' },
|
||||
{ route: FILTERS_URLS.custom_rules, text: 'custom_filtering_rules' },
|
||||
{
|
||||
route: FILTERS_URLS.dns_blocklists,
|
||||
text: 'dns_blocklists',
|
||||
},
|
||||
{
|
||||
route: FILTERS_URLS.dns_allowlists,
|
||||
text: 'dns_allowlists',
|
||||
},
|
||||
{
|
||||
route: FILTERS_URLS.dns_rewrites,
|
||||
text: 'dns_rewrites',
|
||||
},
|
||||
{
|
||||
route: FILTERS_URLS.blocked_services,
|
||||
text: 'blocked_services',
|
||||
},
|
||||
{
|
||||
route: FILTERS_URLS.custom_rules,
|
||||
text: 'custom_filtering_rules',
|
||||
},
|
||||
];
|
||||
|
||||
class Menu extends Component {
|
||||
@@ -52,7 +91,8 @@ class Menu extends Component {
|
||||
|
||||
getActiveClassForDropdown = (URLS) => {
|
||||
const { pathname } = this.props.location;
|
||||
const isActivePage = Object.values(URLS).some((item) => item === pathname);
|
||||
const isActivePage = Object.values(URLS)
|
||||
.some((item) => item === pathname);
|
||||
|
||||
return isActivePage ? 'active' : '';
|
||||
};
|
||||
@@ -79,18 +119,18 @@ class Menu extends Component {
|
||||
getDropdown = ({
|
||||
label, order, URLS, icon, ITEMS,
|
||||
}) => (
|
||||
<Dropdown
|
||||
label={this.props.t(label)}
|
||||
baseClassName={`dropdown nav-item order-${order}`}
|
||||
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
|
||||
icon={icon}>
|
||||
{ITEMS.map((item) => (
|
||||
this.getNavLink({
|
||||
...item,
|
||||
order,
|
||||
className: 'dropdown-item',
|
||||
})))}
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
label={this.props.t(label)}
|
||||
baseClassName='dropdown'
|
||||
controlClassName={`nav-link ${this.getActiveClassForDropdown(URLS)}`}
|
||||
icon={icon}>
|
||||
{ITEMS.map((item) => (
|
||||
this.getNavLink({
|
||||
...item,
|
||||
order,
|
||||
className: 'dropdown-item',
|
||||
})))}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
render() {
|
||||
@@ -99,7 +139,7 @@ class Menu extends Component {
|
||||
'mobile-menu--active': this.props.isMenuOpen,
|
||||
});
|
||||
return (
|
||||
<Fragment>
|
||||
<>
|
||||
<div className={menuClass}>
|
||||
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
|
||||
{MENU_ITEMS.map((item) => (
|
||||
@@ -108,26 +148,33 @@ class Menu extends Component {
|
||||
key={item.text}
|
||||
onClick={this.closeMenu}
|
||||
>
|
||||
{this.getNavLink({ ...item, className: 'nav-link' })}
|
||||
{this.getNavLink({
|
||||
...item,
|
||||
className: 'nav-link',
|
||||
})}
|
||||
</li>
|
||||
))}
|
||||
{this.getDropdown({
|
||||
order: 1,
|
||||
label: 'settings',
|
||||
icon: 'settings',
|
||||
URLS: SETTINGS_URLS,
|
||||
ITEMS: SETTINGS_ITEMS,
|
||||
})}
|
||||
{this.getDropdown({
|
||||
order: 2,
|
||||
label: 'filters',
|
||||
icon: 'filters',
|
||||
URLS: FILTERS_URLS,
|
||||
ITEMS: FILTERS_ITEMS,
|
||||
})}
|
||||
<li className="nav-item order-1">
|
||||
{this.getDropdown({
|
||||
order: 1,
|
||||
label: 'settings',
|
||||
icon: 'settings',
|
||||
URLS: SETTINGS_URLS,
|
||||
ITEMS: SETTINGS_ITEMS,
|
||||
})}
|
||||
</li>
|
||||
<li className="nav-item order-2">
|
||||
{this.getDropdown({
|
||||
order: 2,
|
||||
label: 'filters',
|
||||
icon: 'filters',
|
||||
URLS: FILTERS_URLS,
|
||||
ITEMS: FILTERS_ITEMS,
|
||||
})}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ const getDomainCell = (props) => {
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
const lockIconClass = classNames('icons', 'icon--small', 'd-none', 'd-sm-block', 'cursor--pointer', {
|
||||
const lockIconClass = classNames('icons icon--small d-none d-sm-block cursor--pointer', {
|
||||
'icon--active': answer_dnssec,
|
||||
'icon--disabled': !answer_dnssec,
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
const privacyIconClass = classNames('icons', 'mx-2', 'icon--small', 'd-none', 'd-sm-block', 'cursor--pointer', {
|
||||
const privacyIconClass = classNames('icons mx-2 icon--small d-none d-sm-block cursor--pointer', {
|
||||
'icon--active': hasTracker,
|
||||
'icon--disabled': !hasTracker,
|
||||
'my-3': isDetailed,
|
||||
@@ -56,7 +56,7 @@ const getDomainCell = (props) => {
|
||||
|
||||
const renderGrid = (content, idx) => {
|
||||
const preparedContent = typeof content === 'string' ? t(content) : content;
|
||||
const className = classNames('text-truncate key-colon o-hidden', {
|
||||
const className = classNames('text-truncate o-hidden', {
|
||||
'overflow-break': preparedContent.length > 100,
|
||||
});
|
||||
return <div key={idx} className={className}>{preparedContent}</div>;
|
||||
|
||||
@@ -2,17 +2,20 @@ import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
DEBOUNCE_FILTER_TIMEOUT,
|
||||
DEFAULT_LOGS_FILTER,
|
||||
FORM_NAME,
|
||||
RESPONSE_FILTER,
|
||||
RESPONSE_FILTER_QUERIES,
|
||||
} from '../../../helpers/constants';
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
import IconTooltip from '../../ui/IconTooltip';
|
||||
import { setLogsFilter } from '../../../actions/queryLogs';
|
||||
import useDebounce from '../../../helpers/useDebounce';
|
||||
import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers';
|
||||
|
||||
const renderFilterField = ({
|
||||
input,
|
||||
@@ -25,34 +28,43 @@ const renderFilterField = ({
|
||||
tooltip,
|
||||
meta: { touched, error },
|
||||
onClearInputClick,
|
||||
}) => <>
|
||||
<div className="input-group-search input-group-search__icon--magnifier">
|
||||
<svg className="icons icon--small icon--gray">
|
||||
<use xlinkHref="#magnifier" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
{...input}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
aria-label={placeholder} />
|
||||
<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' />
|
||||
onKeyDown,
|
||||
normalizeOnBlur,
|
||||
}) => {
|
||||
const onBlur = (event) => createOnBlurHandler(event, input, normalizeOnBlur);
|
||||
|
||||
return <>
|
||||
<div className="input-group-search input-group-search__icon--magnifier">
|
||||
<svg className="icons icon--small icon--gray">
|
||||
<use xlinkHref="#magnifier" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
{...input}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
aria-label={placeholder}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<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">
|
||||
<IconTooltip text={tooltip} type='tooltip-custom--logs' />
|
||||
</span>
|
||||
{!disabled
|
||||
&& touched
|
||||
&& (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</>;
|
||||
{!disabled
|
||||
&& touched
|
||||
&& (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</>;
|
||||
};
|
||||
|
||||
renderFilterField.propTypes = {
|
||||
input: PropTypes.object.isRequired,
|
||||
@@ -64,65 +76,91 @@ renderFilterField.propTypes = {
|
||||
disabled: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
onKeyDown: PropTypes.func,
|
||||
normalizeOnBlur: PropTypes.func,
|
||||
meta: PropTypes.shape({
|
||||
touched: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
const FORM_NAMES = {
|
||||
search: 'search',
|
||||
response_status: 'response_status',
|
||||
};
|
||||
|
||||
const Form = (props) => {
|
||||
const {
|
||||
className = '',
|
||||
responseStatusClass,
|
||||
submit,
|
||||
reset,
|
||||
setIsLoading,
|
||||
change,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const debouncedSubmit = debounce(submit, DEBOUNCE_FILTER_TIMEOUT);
|
||||
const zeroDelaySubmit = () => setTimeout(submit, 0);
|
||||
const {
|
||||
response_status, search,
|
||||
} = useSelector((state) => state.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||
|
||||
const clearInput = async () => {
|
||||
await dispatch(setLogsFilter(DEFAULT_LOGS_FILTER));
|
||||
await reset();
|
||||
};
|
||||
const [
|
||||
debouncedSearch,
|
||||
setDebouncedSearch,
|
||||
] = useDebounce(search.trim(), DEBOUNCE_FILTER_TIMEOUT);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setLogsFilter({
|
||||
response_status,
|
||||
search: debouncedSearch,
|
||||
}));
|
||||
|
||||
history.replace(`${getLogsUrlParams(debouncedSearch, response_status)}`);
|
||||
}, [response_status, debouncedSearch]);
|
||||
|
||||
if (response_status && !(response_status in RESPONSE_FILTER_QUERIES)) {
|
||||
change(FORM_NAMES.response_status, DEFAULT_LOGS_FILTER[FORM_NAMES.response_status]);
|
||||
}
|
||||
|
||||
const onInputClear = async () => {
|
||||
setIsLoading(true);
|
||||
await clearInput();
|
||||
setDebouncedSearch(DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => clearInput, []);
|
||||
const onEnterPress = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setDebouncedSearch(search);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeOnBlur = (data) => data.trim();
|
||||
|
||||
return (
|
||||
<form className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
zeroDelaySubmit();
|
||||
debouncedSubmit.cancel();
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
id="search"
|
||||
name="search"
|
||||
id={FORM_NAMES.search}
|
||||
name={FORM_NAMES.search}
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className={classNames('form-control--search form-control--transparent', className)}
|
||||
placeholder={t('domain_or_client')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onChange={debouncedSubmit}
|
||||
onClearInputClick={onInputClear}
|
||||
onKeyDown={onEnterPress}
|
||||
normalizeOnBlur={normalizeOnBlur}
|
||||
/>
|
||||
<div className="field__select">
|
||||
<Field
|
||||
name="response_status"
|
||||
name={FORM_NAMES.response_status}
|
||||
component="select"
|
||||
className={classNames('form-control custom-select custom-select--logs custom-select__arrow--left ml-small form-control--transparent', responseStatusClass)}
|
||||
onChange={zeroDelaySubmit}
|
||||
>
|
||||
{Object.values(RESPONSE_FILTER)
|
||||
.map(({
|
||||
@@ -136,14 +174,13 @@ const Form = (props) => {
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
handleChange: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
responseStatusClass: PropTypes.string,
|
||||
submit: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
change: PropTypes.func.isRequired,
|
||||
setIsLoading: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default reduxForm({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
enableReinitialize: true,
|
||||
})(Form);
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Form from './Form';
|
||||
import { setLogsFilter } from '../../../actions/queryLogs';
|
||||
|
||||
const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
setIsLoading(true);
|
||||
await dispatch(setLogsFilter(values));
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
const Filters = ({ filter, refreshLogs, setIsLoading }) => (
|
||||
<div className="page-header page-header--logs">
|
||||
<h1 className="page-title page-title--large">
|
||||
<Trans>query_log</Trans>
|
||||
@@ -27,17 +16,14 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
||||
<use xlinkHref="#update" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</h1>
|
||||
<Form
|
||||
responseStatusClass="d-sm-block"
|
||||
initialValues={filter}
|
||||
onSubmit={onSubmit}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
Filters.propTypes = {
|
||||
filter: PropTypes.object.isRequired,
|
||||
|
||||
@@ -49,7 +49,7 @@ const Table = (props) => {
|
||||
isLoading,
|
||||
} = props;
|
||||
|
||||
const [t] = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleBlocking = (type, domain) => {
|
||||
const {
|
||||
@@ -239,7 +239,7 @@ const Table = (props) => {
|
||||
sortable={false}
|
||||
resizable={false}
|
||||
data={logs || []}
|
||||
loading={isLoading}
|
||||
loading={isLoading || processingGetLogs}
|
||||
showPageJump={false}
|
||||
showPageSizeOptions={false}
|
||||
onPageChange={changePage}
|
||||
|
||||
@@ -2,11 +2,14 @@ import React, { Fragment, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
import {
|
||||
BLOCK_ACTIONS, smallScreenSize,
|
||||
BLOCK_ACTIONS,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
TABLE_FIRST_PAGE,
|
||||
smallScreenSize,
|
||||
} from '../../helpers/constants';
|
||||
import Loading from '../ui/Loading';
|
||||
import Filters from './Filters';
|
||||
@@ -15,13 +18,15 @@ import Disabled from './Disabled';
|
||||
import { getFilteringStatus } from '../../actions/filtering';
|
||||
import { getClients } from '../../actions';
|
||||
import { getDnsConfig } from '../../actions/dnsConfig';
|
||||
import { getLogsConfig } from '../../actions/queryLogs';
|
||||
import {
|
||||
getLogsConfig,
|
||||
refreshFilteredLogs,
|
||||
resetFilteredLogs,
|
||||
setFilteredLogs,
|
||||
} from '../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../actions/toasts';
|
||||
import './Logs.css';
|
||||
|
||||
const INITIAL_REQUEST = true;
|
||||
const INITIAL_REQUEST_DATA = ['', TABLE_FIRST_PAGE, INITIAL_REQUEST];
|
||||
|
||||
export const processContent = (data, buttonType) => Object.entries(data)
|
||||
.map(([key, value]) => {
|
||||
if (!value) {
|
||||
@@ -56,22 +61,44 @@ export const processContent = (data, buttonType) => Object.entries(data)
|
||||
|
||||
const Logs = (props) => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
response_status: response_status_url_param = '',
|
||||
search: search_url_param = '',
|
||||
} = queryString.parse(history.location.search);
|
||||
|
||||
const { filter } = useSelector((state) => state.queryLogs, shallowEqual);
|
||||
|
||||
const search = filter?.search || search_url_param;
|
||||
const response_status = filter?.response_status || response_status_url_param;
|
||||
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < smallScreenSize);
|
||||
const [detailedDataCurrent, setDetailedDataCurrent] = useState({});
|
||||
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
|
||||
const [isModalOpened, setModalOpened] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
await dispatch(setFilteredLogs({
|
||||
search,
|
||||
response_status,
|
||||
}));
|
||||
setIsLoading(false);
|
||||
})();
|
||||
}, [response_status, search]);
|
||||
|
||||
const {
|
||||
filtering,
|
||||
setLogsPage,
|
||||
setLogsPagination,
|
||||
setLogsFilter,
|
||||
toggleDetailedLogs,
|
||||
dashboard,
|
||||
dnsConfig,
|
||||
queryLogs: {
|
||||
filter,
|
||||
enabled,
|
||||
processingGetConfig,
|
||||
processingAdditionalLogs,
|
||||
@@ -92,16 +119,10 @@ const Logs = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
mediaQuery.addListener(mediaQueryHandler);
|
||||
|
||||
return () => mediaQuery.removeListener(mediaQueryHandler);
|
||||
}, []);
|
||||
|
||||
const closeModal = () => setModalOpened(false);
|
||||
|
||||
const getLogs = (older_than, page, initial) => {
|
||||
if (props.queryLogs.enabled) {
|
||||
if (enabled) {
|
||||
props.getLogs({
|
||||
older_than,
|
||||
page,
|
||||
@@ -112,6 +133,8 @@ const Logs = (props) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
mediaQuery.addEventListener('change', mediaQueryHandler);
|
||||
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
||||
@@ -119,7 +142,6 @@ const Logs = (props) => {
|
||||
dispatch(getClients());
|
||||
try {
|
||||
await Promise.all([
|
||||
getLogs(...INITIAL_REQUEST_DATA),
|
||||
dispatch(getLogsConfig()),
|
||||
dispatch(getDnsConfig()),
|
||||
]);
|
||||
@@ -129,13 +151,18 @@ const Logs = (props) => {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', mediaQueryHandler);
|
||||
dispatch(resetFilteredLogs());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshLogs = async () => {
|
||||
setIsLoading(true);
|
||||
await Promise.all([
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE)),
|
||||
getLogs(...INITIAL_REQUEST_DATA),
|
||||
dispatch(refreshFilteredLogs()),
|
||||
]);
|
||||
dispatch(addSuccessToast('query_log_updated'));
|
||||
setIsLoading(false);
|
||||
@@ -145,13 +172,15 @@ const Logs = (props) => {
|
||||
<>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && (
|
||||
<Fragment>
|
||||
<>
|
||||
<Filters
|
||||
filter={filter}
|
||||
filter={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingAdditionalLogs={processingAdditionalLogs}
|
||||
setLogsFilter={setLogsFilter}
|
||||
refreshLogs={refreshLogs}
|
||||
/>
|
||||
<Table
|
||||
@@ -201,7 +230,7 @@ const Logs = (props) => {
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</Modal>
|
||||
</Fragment>
|
||||
</>
|
||||
)}
|
||||
{!enabled && !processingGetConfig && (
|
||||
<Disabled />
|
||||
@@ -219,7 +248,6 @@ Logs.propTypes = {
|
||||
setRules: PropTypes.func.isRequired,
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
setLogsPagination: PropTypes.func.isRequired,
|
||||
setLogsFilter: PropTypes.func.isRequired,
|
||||
setLogsPage: PropTypes.func.isRequired,
|
||||
toggleDetailedLogs: PropTypes.func.isRequired,
|
||||
dnsConfig: PropTypes.object.isRequired,
|
||||
|
||||
@@ -7,6 +7,7 @@ import Card from '../../ui/Card';
|
||||
import CellWrap from '../../ui/CellWrap';
|
||||
|
||||
import whoisCell from './whoisCell';
|
||||
import LogsSearchLink from '../../ui/LogsSearchLink';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 200;
|
||||
|
||||
@@ -49,7 +50,9 @@ class AutoClients extends Component {
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<div className="logs__text" title={clientStats}>
|
||||
{clientStats}
|
||||
<LogsSearchLink search={row.original.ip}>
|
||||
{clientStats}
|
||||
</LogsSearchLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { normalizeTextarea } from '../../../helpers/helpers';
|
||||
import Card from '../../ui/Card';
|
||||
import Modal from './Modal';
|
||||
import CellWrap from '../../ui/CellWrap';
|
||||
import LogsSearchLink from '../../ui/LogsSearchLink';
|
||||
|
||||
class ClientsTable extends Component {
|
||||
handleFormAdd = (values) => {
|
||||
@@ -49,7 +50,10 @@ class ClientsTable extends Component {
|
||||
};
|
||||
|
||||
getOptionsWithLabels = (options) => (
|
||||
options.map((option) => ({ value: option, label: option }))
|
||||
options.map((option) => ({
|
||||
value: option,
|
||||
label: option,
|
||||
}))
|
||||
);
|
||||
|
||||
getClient = (name, clients) => {
|
||||
@@ -203,7 +207,15 @@ class ClientsTable extends Component {
|
||||
accessor: (row) => this.props.normalizedTopClients.configured[row.name] || 0,
|
||||
sortMethod: (a, b) => b - a,
|
||||
minWidth: 120,
|
||||
Cell: CellWrap,
|
||||
Cell: (row) => {
|
||||
const content = CellWrap(row);
|
||||
|
||||
if (!row.value) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return <LogsSearchLink search={row.original.ids[0]}>{content}</LogsSearchLink>;
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('actions_table_header'),
|
||||
@@ -311,7 +323,6 @@ class ClientsTable extends Component {
|
||||
>
|
||||
<Trans>client_add</Trans>
|
||||
</button>
|
||||
|
||||
<Modal
|
||||
isModalOpen={isModalOpen}
|
||||
modalType={modalType}
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import LogsSearchLink from './LogsSearchLink';
|
||||
import { formatNumber } from '../../helpers/helpers';
|
||||
|
||||
const Cell = ({ value, percent, color }) => (
|
||||
<div className="stats__row">
|
||||
<div className="stats__row-value mb-1">
|
||||
<strong>{formatNumber(value)}</strong>
|
||||
<small className="ml-3 text-muted">{percent}%</small>
|
||||
</div>
|
||||
<div className="progress progress-xs">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
const Cell = ({
|
||||
value, percent, color, search,
|
||||
}) => <div className="stats__row">
|
||||
<div className="stats__row-value mb-1">
|
||||
<strong><LogsSearchLink search={search}>{formatNumber(value)}</LogsSearchLink></strong>
|
||||
<small className="ml-3 text-muted">{percent}%</small>
|
||||
</div>
|
||||
);
|
||||
<div className="progress progress-xs">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
Cell.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
percent: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
search: PropTypes.string,
|
||||
onSearchRedirect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Cell;
|
||||
|
||||
19
client/src/components/ui/IconTooltip.js
Normal file
19
client/src/components/ui/IconTooltip.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './IconTooltip.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const IconTooltip = ({ text, type = '' }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div data-tooltip={t(text)}
|
||||
className={`tooltip-custom ml-1 ${type}`} />;
|
||||
};
|
||||
|
||||
IconTooltip.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
export default IconTooltip;
|
||||
7
client/src/components/ui/LogsSearchLink.css
Normal file
7
client/src/components/ui/LogsSearchLink.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.stats__link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.stats__link:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
33
client/src/components/ui/LogsSearchLink.js
Normal file
33
client/src/components/ui/LogsSearchLink.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import './LogsSearchLink.css';
|
||||
import { getLogsUrlParams } from '../../helpers/helpers';
|
||||
import { MENU_URLS } from '../../helpers/constants';
|
||||
|
||||
const LogsSearchLink = ({
|
||||
search = '', response_status = '', children, link = MENU_URLS.logs,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const to = link === MENU_URLS.logs ? `${MENU_URLS.logs}${getLogsUrlParams(search && `"${search}"`, response_status)}` : link;
|
||||
|
||||
return <Link to={to}
|
||||
className={'stats__link'}
|
||||
tabIndex={0}
|
||||
title={t('click_to_view_queries')}
|
||||
aria-label={t('click_to_view_queries')}>{children}</Link>;
|
||||
};
|
||||
|
||||
LogsSearchLink.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.element]).isRequired,
|
||||
search: PropTypes.string,
|
||||
response_status: PropTypes.string,
|
||||
link: PropTypes.string,
|
||||
};
|
||||
|
||||
export default LogsSearchLink;
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './Tooltip.css';
|
||||
|
||||
const Tooltip = ({ text, type = '' }) => <div data-tooltip={text}
|
||||
className={`tooltip-custom ml-1 ${type}`} />;
|
||||
|
||||
Tooltip.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
Reference in New Issue
Block a user