+ client: handle the new statistics format

This commit is contained in:
Ildar Kamalov 2019-08-22 16:10:47 +03:00
parent 011bc3e36b
commit 6b2496d050
22 changed files with 563 additions and 488 deletions

2
client/package.json vendored
View File

@ -6,7 +6,7 @@
"build-dev": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js", "build-dev": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js",
"watch": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js --watch", "watch": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js --watch",
"build-prod": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.prod.js", "build-prod": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.prod.js",
"lint": "eslint frontend/" "lint": "eslint client/"
}, },
"dependencies": { "dependencies": {
"@nivo/line": "^0.49.1", "@nivo/line": "^0.49.1",

View File

@ -72,12 +72,14 @@
"stats_adult": "Blocked adult websites", "stats_adult": "Blocked adult websites",
"stats_query_domain": "Top queried domains", "stats_query_domain": "Top queried domains",
"for_last_24_hours": "for the last 24 hours", "for_last_24_hours": "for the last 24 hours",
"for_last_days": "for the last {{value}} days",
"no_domains_found": "No domains found", "no_domains_found": "No domains found",
"requests_count": "Requests count", "requests_count": "Requests count",
"top_blocked_domains": "Top blocked domains", "top_blocked_domains": "Top blocked domains",
"top_clients": "Top clients", "top_clients": "Top clients",
"no_clients_found": "No clients found", "no_clients_found": "No clients found",
"general_statistics": "General statistics", "general_statistics": "General statistics",
"number_of_dns_query_days": "A number of DNS quieries processed for the last {{value}} days",
"number_of_dns_query_24_hours": "A number of DNS quieries processed for the last 24 hours", "number_of_dns_query_24_hours": "A number of DNS quieries processed for the last 24 hours",
"number_of_dns_query_blocked_24_hours": "A number of DNS requests blocked by adblock filters and hosts blocklists", "number_of_dns_query_blocked_24_hours": "A number of DNS requests blocked by adblock filters and hosts blocklists",
"number_of_dns_query_blocked_24_hours_by_sec": "A number of DNS requests blocked by the AdGuard browsing security module", "number_of_dns_query_blocked_24_hours_by_sec": "A number of DNS requests blocked by the AdGuard browsing security module",
@ -300,7 +302,6 @@
"client_deleted": "Client \"{{key}}\" successfully deleted", "client_deleted": "Client \"{{key}}\" successfully deleted",
"client_added": "Client \"{{key}}\" successfully added", "client_added": "Client \"{{key}}\" successfully added",
"client_updated": "Client \"{{key}}\" successfully updated", "client_updated": "Client \"{{key}}\" successfully updated",
"table_statistics": "Requests count (last 24 hours)",
"clients_not_found": "No clients found", "clients_not_found": "No clients found",
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?", "client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
"filter_confirm_delete": "Are you sure you want to delete filter?", "filter_confirm_delete": "Are you sure you want to delete filter?",
@ -366,5 +367,6 @@
"config_successfully_saved": "Configuration successfully saved", "config_successfully_saved": "Configuration successfully saved",
"interval_24_hour": "24 hours", "interval_24_hour": "24 hours",
"interval_days": "{{value}} days", "interval_days": "{{value}} days",
"time_period": "Time period" "time_period": "Time period",
"domain": "Domain"
} }

View File

@ -1,11 +1,10 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import round from 'lodash/round';
import { t } from 'i18next'; import { t } from 'i18next';
import { showLoading, hideLoading } from 'react-redux-loading-bar'; import { showLoading, hideLoading } from 'react-redux-loading-bar';
import axios from 'axios'; import axios from 'axios';
import versionCompare from '../helpers/versionCompare'; import versionCompare from '../helpers/versionCompare';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea, sortClients } from '../helpers/helpers'; import { normalizeFilteringStatus, normalizeLogs, normalizeTextarea, sortClients } from '../helpers/helpers';
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants'; import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
import { getTlsStatus } from './encryption'; import { getTlsStatus } from './encryption';
import Api from '../api/Api'; import Api from '../api/Api';
@ -246,27 +245,6 @@ export const getClients = () => async (dispatch) => {
} }
}; };
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
export const getTopStats = () => async (dispatch, getState) => {
dispatch(getTopStatsRequest());
const timer = setInterval(async () => {
const state = getState();
if (state.dashboard.isCoreRunning) {
clearInterval(timer);
try {
const stats = await apiClient.getGlobalStatsTop();
dispatch(getTopStatsSuccess(stats));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getTopStatsFailure(error));
}
}
}, 100);
};
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST'); export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE'); export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS'); export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
@ -314,27 +292,6 @@ export const disableDns = () => async (dispatch) => {
} }
}; };
export const getStatsRequest = createAction('GET_STATS_REQUEST');
export const getStatsFailure = createAction('GET_STATS_FAILURE');
export const getStatsSuccess = createAction('GET_STATS_SUCCESS');
export const getStats = () => async (dispatch) => {
dispatch(getStatsRequest());
try {
const stats = await apiClient.getGlobalStats();
const processedStats = {
...stats,
avg_processing_time: round(stats.avg_processing_time, 2),
};
dispatch(getStatsSuccess(processedStats));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getStatsFailure());
}
};
export const getLogsRequest = createAction('GET_LOGS_REQUEST'); export const getLogsRequest = createAction('GET_LOGS_REQUEST');
export const getLogsFailure = createAction('GET_LOGS_FAILURE'); export const getLogsFailure = createAction('GET_LOGS_FAILURE');
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS'); export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
@ -473,22 +430,6 @@ export const refreshFilters = () => async (dispatch) => {
export const handleRulesChange = createAction('HANDLE_RULES_CHANGE'); export const handleRulesChange = createAction('HANDLE_RULES_CHANGE');
export const getStatsHistoryRequest = createAction('GET_STATS_HISTORY_REQUEST');
export const getStatsHistoryFailure = createAction('GET_STATS_HISTORY_FAILURE');
export const getStatsHistorySuccess = createAction('GET_STATS_HISTORY_SUCCESS');
export const getStatsHistory = () => async (dispatch) => {
dispatch(getStatsHistoryRequest());
try {
const statsHistory = await apiClient.getGlobalStatsHistory();
const normalizedHistory = normalizeHistory(statsHistory);
dispatch(getStatsHistorySuccess(normalizedHistory));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getStatsHistoryFailure());
}
};
export const addFilterRequest = createAction('ADD_FILTER_REQUEST'); export const addFilterRequest = createAction('ADD_FILTER_REQUEST');
export const addFilterFailure = createAction('ADD_FILTER_FAILURE'); export const addFilterFailure = createAction('ADD_FILTER_FAILURE');
export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS'); export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');

View File

@ -1,6 +1,8 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import Api from '../api/Api'; import Api from '../api/Api';
import { addErrorToast, addSuccessToast } from './index'; import { addErrorToast, addSuccessToast } from './index';
import { normalizeTopStats } from '../helpers/helpers';
const apiClient = new Api(); const apiClient = new Api();
@ -34,3 +36,26 @@ export const setStatsConfig = config => async (dispatch) => {
dispatch(setStatsConfigFailure()); dispatch(setStatsConfigFailure());
} }
}; };
export const getStatsRequest = createAction('GET_STATS_REQUEST');
export const getStatsFailure = createAction('GET_STATS_FAILURE');
export const getStatsSuccess = createAction('GET_STATS_SUCCESS');
export const getStats = () => async (dispatch) => {
dispatch(getStatsRequest());
try {
const stats = await apiClient.getStats();
const normalizedStats = {
...stats,
top_blocked_domains: normalizeTopStats(stats.top_blocked_domains),
top_clients: normalizeTopStats(stats.top_clients),
top_queried_domains: normalizeTopStats(stats.top_queried_domains),
};
dispatch(getStatsSuccess(normalizedStats));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getStatsFailure());
}
};

View File

@ -1,6 +1,4 @@
import axios from 'axios'; import axios from 'axios';
import subHours from 'date-fns/sub_hours';
import dateFormat from 'date-fns/format';
export default class Api { export default class Api {
baseUrl = 'control'; baseUrl = 'control';
@ -26,11 +24,8 @@ export default class Api {
// Global methods // Global methods
GLOBAL_RESTART = { path: 'restart', method: 'POST' }; GLOBAL_RESTART = { path: 'restart', method: 'POST' };
GLOBAL_START = { path: 'start', method: 'POST' }; GLOBAL_START = { path: 'start', method: 'POST' };
GLOBAL_STATS = { path: 'stats', method: 'GET' };
GLOBAL_STATS_HISTORY = { path: 'stats_history', method: 'GET' };
GLOBAL_STATUS = { path: 'status', method: 'GET' }; GLOBAL_STATUS = { path: 'status', method: 'GET' };
GLOBAL_STOP = { path: 'stop', method: 'POST' }; GLOBAL_STOP = { path: 'stop', method: 'POST' };
GLOBAL_STATS_TOP = { path: 'stats_top', method: 'GET' };
GLOBAL_QUERY_LOG = { path: 'querylog', method: 'GET' }; GLOBAL_QUERY_LOG = { path: 'querylog', method: 'GET' };
GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', method: 'POST' }; GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', method: 'POST' };
GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' }; GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' };
@ -56,36 +51,11 @@ export default class Api {
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
getGlobalStats() {
const { path, method } = this.GLOBAL_STATS;
return this.makeRequest(path, method);
}
getGlobalStatsHistory() {
const { path, method } = this.GLOBAL_STATS_HISTORY;
const format = 'YYYY-MM-DDTHH:mm:ssZ';
const dateNow = Date.now();
const config = {
params: {
start_time: dateFormat(subHours(dateNow, 24), format),
end_time: dateFormat(dateNow, format),
time_unit: 'hours',
},
};
return this.makeRequest(path, method, config);
}
getGlobalStatus() { getGlobalStatus() {
const { path, method } = this.GLOBAL_STATUS; const { path, method } = this.GLOBAL_STATUS;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
getGlobalStatsTop() {
const { path, method } = this.GLOBAL_STATS_TOP;
return this.makeRequest(path, method);
}
getQueryLog() { getQueryLog() {
const { path, method } = this.GLOBAL_QUERY_LOG; const { path, method } = this.GLOBAL_QUERY_LOG;
return this.makeRequest(path, method); return this.makeRequest(path, method);
@ -529,9 +499,15 @@ export default class Api {
} }
// Settings for statistics // Settings for statistics
GET_STATS = { path: 'stats', method: 'GET' };
STATS_INFO = { path: 'stats_info', method: 'GET' }; STATS_INFO = { path: 'stats_info', method: 'GET' };
STATS_CONFIG = { path: 'stats_config', method: 'POST' }; STATS_CONFIG = { path: 'stats_config', method: 'POST' };
getStats() {
const { path, method } = this.GET_STATS;
return this.makeRequest(path, method);
}
getStatsInfo() { getStatsInfo() {
const { path, method } = this.STATS_INFO; const { path, method } = this.STATS_INFO;
return this.makeRequest(path, method); return this.makeRequest(path, method);

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import map from 'lodash/map';
import { withNamespaces, Trans } from 'react-i18next'; import { withNamespaces, Trans } from 'react-i18next';
import Card from '../ui/Card'; import Card from '../ui/Card';
@ -13,52 +12,58 @@ import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants'; import { STATUS_COLORS } from '../../helpers/constants';
class BlockedDomains extends Component { class BlockedDomains extends Component {
columns = [{ columns = [
Header: 'IP', {
accessor: 'ip', Header: <Trans>domain</Trans>,
Cell: (row) => { accessor: 'domain',
const { value } = row; Cell: (row) => {
const trackerData = getTrackerData(value); const { value } = row;
const trackerData = getTrackerData(value);
return ( return (
<div className="logs__row"> <div className="logs__row">
<div className="logs__text" title={value}> <div className="logs__text" title={value}>
{value} {value}
</div>
{trackerData && <Popover data={trackerData} />}
</div> </div>
{trackerData && <Popover data={trackerData} />} );
</div> },
);
}, },
}, { {
Header: <Trans>requests_count</Trans>, Header: <Trans>requests_count</Trans>,
accessor: 'domain', accessor: 'count',
maxWidth: 190, maxWidth: 190,
Cell: ({ value }) => { Cell: ({ value }) => {
const { const { blockedFiltering, replacedSafebrowsing, replacedParental } = this.props;
blockedFiltering, const blocked = blockedFiltering + replacedSafebrowsing + replacedParental;
replacedSafebrowsing, const percent = getPercent(blocked, value);
replacedParental,
} = this.props;
const blocked = blockedFiltering + replacedSafebrowsing + replacedParental;
const percent = getPercent(blocked, value);
return ( return <Cell value={value} percent={percent} color={STATUS_COLORS.red} />;
<Cell value={value} percent={percent} color={STATUS_COLORS.red} /> },
);
}, },
}]; ];
render() { render() {
const { t } = this.props; const {
t, refreshButton, topBlockedDomains, subtitle,
} = this.props;
return ( return (
<Card title={ t('top_blocked_domains') } subtitle={ t('for_last_24_hours') } bodyType="card-table" refresh={this.props.refreshButton}> <Card
title={t('top_blocked_domains')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<ReactTable <ReactTable
data={map(this.props.topBlockedDomains, (value, prop) => ( data={topBlockedDomains.map(item => ({
{ ip: prop, domain: value } domain: item.name,
))} count: item.count,
}))}
columns={this.columns} columns={this.columns}
showPagination={false} showPagination={false}
noDataText={ t('no_domains_found') } noDataText={t('no_domains_found')}
minRows={6} minRows={6}
className="-striped -highlight card-table-overflow stats__table" className="-striped -highlight card-table-overflow stats__table"
/> />
@ -68,12 +73,13 @@ class BlockedDomains extends Component {
} }
BlockedDomains.propTypes = { BlockedDomains.propTypes = {
topBlockedDomains: PropTypes.object.isRequired, topBlockedDomains: PropTypes.array.isRequired,
blockedFiltering: PropTypes.number.isRequired, blockedFiltering: PropTypes.number.isRequired,
replacedSafebrowsing: PropTypes.number.isRequired, replacedSafebrowsing: PropTypes.number.isRequired,
replacedParental: PropTypes.number.isRequired, replacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired, refreshButton: PropTypes.node.isRequired,
t: PropTypes.func, subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
}; };
export default withNamespaces()(BlockedDomains); export default withNamespaces()(BlockedDomains);

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import map from 'lodash/map';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card'; import Card from '../ui/Card';
@ -18,55 +17,71 @@ class Clients extends Component {
return STATUS_COLORS.yellow; return STATUS_COLORS.yellow;
} }
return STATUS_COLORS.red; return STATUS_COLORS.red;
} };
columns = [{ columns = [
Header: 'IP', {
accessor: 'ip', Header: 'IP',
Cell: ({ value }) => { accessor: 'ip',
const clientName = getClientName(this.props.clients, value) Cell: ({ value }) => {
|| getClientName(this.props.autoClients, value); const clientName =
let client; getClientName(this.props.clients, value) ||
getClientName(this.props.autoClients, value);
let client;
if (clientName) { if (clientName) {
client = <span>{clientName} <small>({value})</small></span>; client = (
} else { <span>
client = value; {clientName} <small>({value})</small>
} </span>
);
} else {
client = value;
}
return ( return (
<div className="logs__row logs__row--overflow"> <div className="logs__row logs__row--overflow">
<span className="logs__text" title={value}> <span className="logs__text" title={value}>
{client} {client}
</span> </span>
</div> </div>
); );
},
sortMethod: (a, b) =>
parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
}, },
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10), {
}, { Header: <Trans>requests_count</Trans>,
Header: <Trans>requests_count</Trans>, accessor: 'count',
accessor: 'count', Cell: ({ value }) => {
Cell: ({ value }) => { const percent = getPercent(this.props.dnsQueries, value);
const percent = getPercent(this.props.dnsQueries, value); const percentColor = this.getPercentColor(percent);
const percentColor = this.getPercentColor(percent);
return ( return <Cell value={value} percent={percent} color={percentColor} />;
<Cell value={value} percent={percent} color={percentColor} /> },
);
}, },
}]; ];
render() { render() {
const { t } = this.props; const {
t, refreshButton, topClients, subtitle,
} = this.props;
return ( return (
<Card title={ t('top_clients') } subtitle={ t('for_last_24_hours') } bodyType="card-table" refresh={this.props.refreshButton}> <Card
title={t('top_clients')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<ReactTable <ReactTable
data={map(this.props.topClients, (value, prop) => ( data={topClients.map(item => ({
{ ip: prop, count: value } ip: item.name,
))} count: item.count,
}))}
columns={this.columns} columns={this.columns}
showPagination={false} showPagination={false}
noDataText={ t('no_clients_found') } noDataText={t('no_clients_found')}
minRows={6} minRows={6}
className="-striped -highlight card-table-overflow" className="-striped -highlight card-table-overflow"
/> />
@ -76,12 +91,13 @@ class Clients extends Component {
} }
Clients.propTypes = { Clients.propTypes = {
topClients: PropTypes.object.isRequired, topClients: PropTypes.array.isRequired,
dnsQueries: PropTypes.number.isRequired, dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired, refreshButton: PropTypes.node.isRequired,
clients: PropTypes.array.isRequired, clients: PropTypes.array.isRequired,
autoClients: PropTypes.array.isRequired, autoClients: PropTypes.array.isRequired,
t: PropTypes.func, subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
}; };
export default withNamespaces()(Clients); export default withNamespaces()(Clients);

View File

@ -1,88 +1,116 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import round from 'lodash/round';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Tooltip from '../ui/Tooltip'; import Tooltip from '../ui/Tooltip';
const tooltipType = 'tooltip-custom--narrow'; const tooltipType = 'tooltip-custom--narrow';
const Counters = props => ( const Counters = (props) => {
<Card title={ props.t('general_statistics') } subtitle={ props.t('for_last_24_hours') } bodyType="card-table" refresh={props.refreshButton}> const {
<table className="table card-table"> t,
<tbody> interval,
<tr> refreshButton,
<td> subtitle,
<Trans>dns_query</Trans> dnsQueries,
<Tooltip text={ props.t('number_of_dns_query_24_hours') } type={tooltipType} /> blockedFiltering,
</td> replacedSafebrowsing,
<td className="text-right"> replacedParental,
<span className="text-muted"> replacedSafesearch,
{props.dnsQueries} avgProcessingTime,
</span> } = props;
</td>
</tr> const tooltipTitle =
<tr> interval === 1
<td> ? t('number_of_dns_query_24_hours')
<a href="#filters"> : t('number_of_dns_query_days', { value: interval });
<Trans>blocked_by</Trans>
</a> return (
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours') } type={tooltipType} /> <Card
</td> title={t('general_statistics')}
<td className="text-right"> subtitle={subtitle}
<span className="text-muted"> bodyType="card-table"
{props.blockedFiltering} refresh={refreshButton}
</span> >
</td> <table className="table card-table">
</tr> <tbody>
<tr> <tr>
<td> <td>
<Trans>stats_malware_phishing</Trans> <Trans>dns_query</Trans>
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours_by_sec') } type={tooltipType} /> <Tooltip text={tooltipTitle} type={tooltipType} />
</td> </td>
<td className="text-right"> <td className="text-right">
<span className="text-muted"> <span className="text-muted">{dnsQueries}</span>
{props.replacedSafebrowsing} </td>
</span> </tr>
</td> <tr>
</tr> <td>
<tr> <a href="#filters">
<td> <Trans>blocked_by</Trans>
<Trans>stats_adult</Trans> </a>
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours_adult') } type={tooltipType} /> <Tooltip
</td> text={t('number_of_dns_query_blocked_24_hours')}
<td className="text-right"> type={tooltipType}
<span className="text-muted"> />
{props.replacedParental} </td>
</span> <td className="text-right">
</td> <span className="text-muted">{blockedFiltering}</span>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>enforced_save_search</Trans> <td>
<Tooltip text={ props.t('number_of_dns_query_to_safe_search') } type={tooltipType} /> <Trans>stats_malware_phishing</Trans>
</td> <Tooltip
<td className="text-right"> text={t('number_of_dns_query_blocked_24_hours_by_sec')}
<span className="text-muted"> type={tooltipType}
{props.replacedSafesearch} />
</span> </td>
</td> <td className="text-right">
</tr> <span className="text-muted">{replacedSafebrowsing}</span>
<tr> </td>
<td> </tr>
<Trans>average_processing_time</Trans> <tr>
<Tooltip text={ props.t('average_processing_time_hint') } type={tooltipType} /> <td>
</td> <Trans>stats_adult</Trans>
<td className="text-right"> <Tooltip
<span className="text-muted"> text={t('number_of_dns_query_blocked_24_hours_adult')}
{props.avgProcessingTime} type={tooltipType}
</span> />
</td> </td>
</tr> <td className="text-right">
</tbody> <span className="text-muted">{replacedParental}</span>
</table> </td>
</Card> </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">{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, 2)} ms` : 0}
</span>
</td>
</tr>
</tbody>
</table>
</Card>
);
};
Counters.propTypes = { Counters.propTypes = {
dnsQueries: PropTypes.number.isRequired, dnsQueries: PropTypes.number.isRequired,
@ -92,6 +120,8 @@ Counters.propTypes = {
replacedSafesearch: PropTypes.number.isRequired, replacedSafesearch: PropTypes.number.isRequired,
avgProcessingTime: PropTypes.number.isRequired, avgProcessingTime: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired, refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
interval: PropTypes.number.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
}; };

View File

@ -20,3 +20,8 @@
border-top: 6px solid transparent; border-top: 6px solid transparent;
border-bottom: 6px solid #585965; border-bottom: 6px solid #585965;
} }
.card-chart-bg {
left: -20px;
width: calc(100% + 20px);
}

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import map from 'lodash/map';
import { withNamespaces, Trans } from 'react-i18next'; import { withNamespaces, Trans } from 'react-i18next';
import Card from '../ui/Card'; import Card from '../ui/Card';
@ -20,49 +19,58 @@ class QueriedDomains extends Component {
return STATUS_COLORS.yellow; return STATUS_COLORS.yellow;
} }
return STATUS_COLORS.green; return STATUS_COLORS.green;
} };
columns = [{ columns = [
Header: 'IP', {
accessor: 'ip', Header: <Trans>domain</Trans>,
Cell: (row) => { accessor: 'domain',
const { value } = row; Cell: (row) => {
const trackerData = getTrackerData(value); const { value } = row;
const trackerData = getTrackerData(value);
return ( return (
<div className="logs__row"> <div className="logs__row">
<div className="logs__text" title={value}> <div className="logs__text" title={value}>
{value} {value}
</div>
{trackerData && <Popover data={trackerData} />}
</div> </div>
{trackerData && <Popover data={trackerData} />} );
</div> },
);
}, },
}, { {
Header: <Trans>requests_count</Trans>, Header: <Trans>requests_count</Trans>,
accessor: 'count', accessor: 'count',
maxWidth: 190, maxWidth: 190,
Cell: ({ value }) => { Cell: ({ value }) => {
const percent = getPercent(this.props.dnsQueries, value); const percent = getPercent(this.props.dnsQueries, value);
const percentColor = this.getPercentColor(percent); const percentColor = this.getPercentColor(percent);
return ( return <Cell value={value} percent={percent} color={percentColor} />;
<Cell value={value} percent={percent} color={percentColor} /> },
);
}, },
}]; ];
render() { render() {
const { t } = this.props; const {
t, refreshButton, topQueriedDomains, subtitle,
} = this.props;
return ( return (
<Card title={ t('stats_query_domain') } subtitle={ t('for_last_24_hours') } bodyType="card-table" refresh={this.props.refreshButton}> <Card
title={t('stats_query_domain')}
subtitle={subtitle}
bodyType="card-table"
refresh={refreshButton}
>
<ReactTable <ReactTable
data={map(this.props.topQueriedDomains, (value, prop) => ( data={topQueriedDomains.map(item => ({
{ ip: prop, count: value } domain: item.name,
))} count: item.count,
}))}
columns={this.columns} columns={this.columns}
showPagination={false} showPagination={false}
noDataText={ t('no_domains_found') } noDataText={t('no_domains_found')}
minRows={6} minRows={6}
className="-striped -highlight card-table-overflow stats__table" className="-striped -highlight card-table-overflow stats__table"
/> />
@ -72,10 +80,11 @@ class QueriedDomains extends Component {
} }
QueriedDomains.propTypes = { QueriedDomains.propTypes = {
topQueriedDomains: PropTypes.object.isRequired, topQueriedDomains: PropTypes.array.isRequired,
dnsQueries: PropTypes.number.isRequired, dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired, refreshButton: PropTypes.node.isRequired,
t: PropTypes.func, subtitle: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
}; };
export default withNamespaces()(QueriedDomains); export default withNamespaces()(QueriedDomains);

View File

@ -5,37 +5,42 @@ import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Line from '../ui/Line'; import Line from '../ui/Line';
import { getPercent } from '../../helpers/helpers'; import { getPercent, normalizeHistory } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants'; import { STATUS_COLORS } from '../../helpers/constants';
class Statistics extends Component { class Statistics extends Component {
getNormalizedHistory = (data, interval, id) => [{ data: normalizeHistory(data, interval), id }];
render() { render() {
const { const {
interval,
dnsQueries, dnsQueries,
blockedFiltering, blockedFiltering,
replacedSafebrowsing, replacedSafebrowsing,
replacedParental, replacedParental,
numDnsQueries,
numBlockedFiltering,
numReplacedSafebrowsing,
numReplacedParental,
} = this.props; } = this.props;
const filteringData = [this.props.history[1]];
const queriesData = [this.props.history[2]];
const parentalData = [this.props.history[3]];
const safebrowsingData = [this.props.history[4]];
return ( return (
<div className="row"> <div className="row">
<div className="col-sm-6 col-lg-3"> <div className="col-sm-6 col-lg-3">
<Card type="card--full" bodyType="card-wrap"> <Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats"> <div className="card-body-stats">
<div className="card-value card-value-stats text-blue"> <div className="card-value card-value-stats text-blue">
{dnsQueries} {numDnsQueries}
</div> </div>
<div className="card-title-stats"> <div className="card-title-stats">
<Trans>dns_query</Trans> <Trans>dns_query</Trans>
</div> </div>
</div> </div>
<div className="card-chart-bg"> <div className="card-chart-bg">
<Line data={queriesData} color={STATUS_COLORS.blue}/> <Line
data={this.getNormalizedHistory(dnsQueries, interval, 'dnsQueries')}
color={STATUS_COLORS.blue}
/>
</div> </div>
</Card> </Card>
</div> </div>
@ -43,10 +48,10 @@ class Statistics extends Component {
<Card type="card--full" bodyType="card-wrap"> <Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats"> <div className="card-body-stats">
<div className="card-value card-value-stats text-red"> <div className="card-value card-value-stats text-red">
{blockedFiltering} {numBlockedFiltering}
</div> </div>
<div className="card-value card-value-percent text-red"> <div className="card-value card-value-percent text-red">
{getPercent(dnsQueries, blockedFiltering)} {getPercent(numDnsQueries, numBlockedFiltering)}
</div> </div>
<div className="card-title-stats"> <div className="card-title-stats">
<a href="#filters"> <a href="#filters">
@ -55,7 +60,14 @@ class Statistics extends Component {
</div> </div>
</div> </div>
<div className="card-chart-bg"> <div className="card-chart-bg">
<Line data={filteringData} color={STATUS_COLORS.red}/> <Line
data={this.getNormalizedHistory(
blockedFiltering,
interval,
'blockedFiltering',
)}
color={STATUS_COLORS.red}
/>
</div> </div>
</Card> </Card>
</div> </div>
@ -63,17 +75,24 @@ class Statistics extends Component {
<Card type="card--full" bodyType="card-wrap"> <Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats"> <div className="card-body-stats">
<div className="card-value card-value-stats text-green"> <div className="card-value card-value-stats text-green">
{replacedSafebrowsing} {numReplacedSafebrowsing}
</div> </div>
<div className="card-value card-value-percent text-green"> <div className="card-value card-value-percent text-green">
{getPercent(dnsQueries, replacedSafebrowsing)} {getPercent(numDnsQueries, numReplacedSafebrowsing)}
</div> </div>
<div className="card-title-stats"> <div className="card-title-stats">
<Trans>stats_malware_phishing</Trans> <Trans>stats_malware_phishing</Trans>
</div> </div>
</div> </div>
<div className="card-chart-bg"> <div className="card-chart-bg">
<Line data={safebrowsingData} color={STATUS_COLORS.green}/> <Line
data={this.getNormalizedHistory(
replacedSafebrowsing,
interval,
'replacedSafebrowsing',
)}
color={STATUS_COLORS.green}
/>
</div> </div>
</Card> </Card>
</div> </div>
@ -81,17 +100,24 @@ class Statistics extends Component {
<Card type="card--full" bodyType="card-wrap"> <Card type="card--full" bodyType="card-wrap">
<div className="card-body-stats"> <div className="card-body-stats">
<div className="card-value card-value-stats text-yellow"> <div className="card-value card-value-stats text-yellow">
{replacedParental} {numReplacedParental}
</div> </div>
<div className="card-value card-value-percent text-yellow"> <div className="card-value card-value-percent text-yellow">
{getPercent(dnsQueries, replacedParental)} {getPercent(numDnsQueries, numReplacedParental)}
</div> </div>
<div className="card-title-stats"> <div className="card-title-stats">
<Trans>stats_adult</Trans> <Trans>stats_adult</Trans>
</div> </div>
</div> </div>
<div className="card-chart-bg"> <div className="card-chart-bg">
<Line data={parentalData} color={STATUS_COLORS.yellow}/> <Line
data={this.getNormalizedHistory(
replacedParental,
interval,
'replacedParental',
)}
color={STATUS_COLORS.yellow}
/>
</div> </div>
</Card> </Card>
</div> </div>
@ -101,11 +127,15 @@ class Statistics extends Component {
} }
Statistics.propTypes = { Statistics.propTypes = {
history: PropTypes.array.isRequired, interval: PropTypes.number.isRequired,
dnsQueries: PropTypes.number.isRequired, dnsQueries: PropTypes.array.isRequired,
blockedFiltering: PropTypes.number.isRequired, blockedFiltering: PropTypes.array.isRequired,
replacedSafebrowsing: PropTypes.number.isRequired, replacedSafebrowsing: PropTypes.array.isRequired,
replacedParental: PropTypes.number.isRequired, replacedParental: PropTypes.array.isRequired,
numDnsQueries: PropTypes.number.isRequired,
numBlockedFiltering: PropTypes.number.isRequired,
numReplacedSafebrowsing: PropTypes.number.isRequired,
numReplacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired, refreshButton: PropTypes.node.isRequired,
}; };

View File

@ -19,10 +19,9 @@ class Dashboard extends Component {
getAllStats = () => { getAllStats = () => {
this.props.getStats(); this.props.getStats();
this.props.getStatsHistory(); this.props.getStatsConfig();
this.props.getTopStats();
this.props.getClients(); this.props.getClients();
} };
getToggleFilteringButton = () => { getToggleFilteringButton = () => {
const { protectionEnabled, processingProtection } = this.props.dashboard; const { protectionEnabled, processingProtection } = this.props.dashboard;
@ -39,16 +38,20 @@ class Dashboard extends Component {
<Trans>{buttonText}</Trans> <Trans>{buttonText}</Trans>
</button> </button>
); );
} };
render() { render() {
const { dashboard, t } = this.props; const { dashboard, stats, t } = this.props;
const dashboardProcessing = const dashboardProcessing =
dashboard.processing || dashboard.processing ||
dashboard.processingStats ||
dashboard.processingStatsHistory ||
dashboard.processingClients || dashboard.processingClients ||
dashboard.processingTopStats; stats.processingStats ||
stats.processingGetConfig;
const subtitle =
stats.interval === 1
? t('for_last_24_hours')
: t('for_last_days', { value: stats.interval });
const refreshFullButton = ( const refreshFullButton = (
<button <button
@ -59,6 +62,7 @@ class Dashboard extends Component {
<Trans>refresh_statics</Trans> <Trans>refresh_statics</Trans>
</button> </button>
); );
const refreshButton = ( const refreshButton = (
<button <button
type="button" type="button"
@ -73,87 +77,85 @@ class Dashboard extends Component {
return ( return (
<Fragment> <Fragment>
<PageTitle title={ t('dashboard') }> <PageTitle title={t('dashboard')}>
<div className="page-title__actions"> <div className="page-title__actions">
{this.getToggleFilteringButton()} {this.getToggleFilteringButton()}
{refreshFullButton} {refreshFullButton}
</div> </div>
</PageTitle> </PageTitle>
{dashboardProcessing && <Loading />} {dashboardProcessing && <Loading />}
{!dashboardProcessing && {!dashboardProcessing && (
<div className="row row-cards"> <div className="row row-cards">
{dashboard.statsHistory && <div className="col-lg-12">
<div className="col-lg-12"> <Statistics
<Statistics interval={stats.interval}
history={dashboard.statsHistory} dnsQueries={stats.dnsQueries}
refreshButton={refreshButton} blockedFiltering={stats.blockedFiltering}
dnsQueries={dashboard.stats.dns_queries} replacedSafebrowsing={stats.replacedSafebrowsing}
blockedFiltering={dashboard.stats.blocked_filtering} replacedParental={stats.replacedParental}
replacedSafebrowsing={dashboard.stats.replaced_safebrowsing} numDnsQueries={stats.numDnsQueries}
replacedParental={dashboard.stats.replaced_parental} numBlockedFiltering={stats.numBlockedFiltering}
/> numReplacedSafebrowsing={stats.numReplacedSafebrowsing}
</div> numReplacedParental={stats.numReplacedParental}
} refreshButton={refreshButton}
<div className="col-lg-6"> />
{dashboard.stats && </div>
<Counters <div className="col-lg-6">
refreshButton={refreshButton} <Counters
dnsQueries={dashboard.stats.dns_queries} subtitle={subtitle}
blockedFiltering={dashboard.stats.blocked_filtering} interval={stats.interval}
replacedSafebrowsing={dashboard.stats.replaced_safebrowsing} dnsQueries={stats.numDnsQueries}
replacedParental={dashboard.stats.replaced_parental} blockedFiltering={stats.numBlockedFiltering}
replacedSafesearch={dashboard.stats.replaced_safesearch} replacedSafebrowsing={stats.numReplacedSafebrowsing}
avgProcessingTime={dashboard.stats.avg_processing_time} replacedParental={stats.numReplacedParental}
/> replacedSafesearch={stats.numReplacedSafesearch}
} avgProcessingTime={stats.avgProcessingTime}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<Clients
subtitle={subtitle}
dnsQueries={stats.numDnsQueries}
topClients={stats.topClients}
clients={dashboard.clients}
autoClients={dashboard.autoClients}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<QueriedDomains
subtitle={subtitle}
dnsQueries={stats.numDnsQueries}
topQueriedDomains={stats.topQueriedDomains}
refreshButton={refreshButton}
/>
</div>
<div className="col-lg-6">
<BlockedDomains
subtitle={subtitle}
topBlockedDomains={stats.topBlockedDomains}
blockedFiltering={stats.numBlockedFiltering}
replacedSafebrowsing={stats.numReplacedSafebrowsing}
replacedParental={stats.numReplacedParental}
refreshButton={refreshButton}
/>
</div> </div>
{dashboard.topStats &&
<Fragment>
<div className="col-lg-6">
<Clients
dnsQueries={dashboard.stats.dns_queries}
refreshButton={refreshButton}
topClients={dashboard.topStats.top_clients}
clients={dashboard.clients}
autoClients={dashboard.autoClients}
/>
</div>
<div className="col-lg-6">
<QueriedDomains
dnsQueries={dashboard.stats.dns_queries}
refreshButton={refreshButton}
topQueriedDomains={dashboard.topStats.top_queried_domains}
/>
</div>
<div className="col-lg-6">
<BlockedDomains
refreshButton={refreshButton}
topBlockedDomains={dashboard.topStats.top_blocked_domains}
blockedFiltering={dashboard.stats.blocked_filtering}
replacedSafebrowsing={dashboard.stats.replaced_safebrowsing}
replacedParental={dashboard.stats.replaced_parental}
/>
</div>
</Fragment>
}
</div> </div>
} )}
</Fragment> </Fragment>
); );
} }
} }
Dashboard.propTypes = { Dashboard.propTypes = {
getStats: PropTypes.func, dashboard: PropTypes.object.isRequired,
getStatsHistory: PropTypes.func, stats: PropTypes.object.isRequired,
getTopStats: PropTypes.func, getStats: PropTypes.func.isRequired,
dashboard: PropTypes.object, getStatsConfig: PropTypes.func.isRequired,
isCoreRunning: PropTypes.bool, toggleProtection: PropTypes.func.isRequired,
getFiltering: PropTypes.func, getClients: PropTypes.func.isRequired,
toggleProtection: PropTypes.func, t: PropTypes.func.isRequired,
getClients: PropTypes.func,
processingProtection: PropTypes.bool,
t: PropTypes.func,
}; };
export default withNamespaces()(Dashboard); export default withNamespaces()(Dashboard);

View File

@ -27,8 +27,9 @@ class AutoClients extends Component {
}; };
getStats = (ip, stats) => { getStats = (ip, stats) => {
if (stats && stats.top_clients) { if (stats) {
return stats.top_clients[ip]; const statsForCurrentIP = stats.find(item => item.name === ip);
return statsForCurrentIP && statsForCurrentIP.count;
} }
return ''; return '';
@ -59,11 +60,11 @@ class AutoClients extends Component {
Cell: this.cellWrap, Cell: this.cellWrap,
}, },
{ {
Header: this.props.t('table_statistics'), Header: this.props.t('requests_count'),
accessor: 'statistics', accessor: 'statistics',
Cell: (row) => { Cell: (row) => {
const clientIP = row.original.ip; const clientIP = row.original.ip;
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats); const clientStats = clientIP && this.getStats(clientIP, this.props.topClients);
if (clientStats) { if (clientStats) {
return ( return (
@ -112,7 +113,7 @@ class AutoClients extends Component {
AutoClients.propTypes = { AutoClients.propTypes = {
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
autoClients: PropTypes.array.isRequired, autoClients: PropTypes.array.isRequired,
topStats: PropTypes.object.isRequired, topClients: PropTypes.array.isRequired,
}; };
export default withNamespaces()(AutoClients); export default withNamespaces()(AutoClients);

View File

@ -63,8 +63,9 @@ class ClientsTable extends Component {
}; };
getStats = (ip, stats) => { getStats = (ip, stats) => {
if (stats && stats.top_clients) { if (stats) {
return stats.top_clients[ip]; const statsForCurrentIP = stats.find(item => item.name === ip);
return statsForCurrentIP && statsForCurrentIP.count;
} }
return ''; return '';
@ -149,11 +150,11 @@ class ClientsTable extends Component {
}, },
}, },
{ {
Header: this.props.t('table_statistics'), Header: this.props.t('requests_count'),
accessor: 'statistics', accessor: 'statistics',
Cell: (row) => { Cell: (row) => {
const clientIP = row.original.ip; const clientIP = row.original.ip;
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats); const clientStats = clientIP && this.getStats(clientIP, this.props.topClients);
if (clientStats) { if (clientStats) {
return ( return (
@ -276,7 +277,7 @@ class ClientsTable extends Component {
ClientsTable.propTypes = { ClientsTable.propTypes = {
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
clients: PropTypes.array.isRequired, clients: PropTypes.array.isRequired,
topStats: PropTypes.object.isRequired, topClients: PropTypes.array.isRequired,
toggleClientModal: PropTypes.func.isRequired, toggleClientModal: PropTypes.func.isRequired,
deleteClient: PropTypes.func.isRequired, deleteClient: PropTypes.func.isRequired,
addClient: PropTypes.func.isRequired, addClient: PropTypes.func.isRequired,

View File

@ -10,13 +10,14 @@ import Loading from '../../ui/Loading';
class Clients extends Component { class Clients extends Component {
componentDidMount() { componentDidMount() {
this.props.getClients(); this.props.getClients();
this.props.getTopStats(); this.props.getStats();
} }
render() { render() {
const { const {
t, t,
dashboard, dashboard,
stats,
clients, clients,
addClient, addClient,
updateClient, updateClient,
@ -27,12 +28,12 @@ class Clients extends Component {
return ( return (
<Fragment> <Fragment>
<PageTitle title={t('client_settings')} /> <PageTitle title={t('client_settings')} />
{(dashboard.processingTopStats || dashboard.processingClients) && <Loading />} {(stats.processingStats || dashboard.processingClients) && <Loading />}
{!dashboard.processingTopStats && !dashboard.processingClients && ( {!stats.processingStats && !dashboard.processingClients && (
<Fragment> <Fragment>
<ClientsTable <ClientsTable
clients={dashboard.clients} clients={dashboard.clients}
topStats={dashboard.topStats} topClients={stats.topClients}
isModalOpen={clients.isModalOpen} isModalOpen={clients.isModalOpen}
modalClientName={clients.modalClientName} modalClientName={clients.modalClientName}
modalType={clients.modalType} modalType={clients.modalType}
@ -46,7 +47,7 @@ class Clients extends Component {
/> />
<AutoClients <AutoClients
autoClients={dashboard.autoClients} autoClients={dashboard.autoClients}
topStats={dashboard.topStats} topClients={stats.topClients}
/> />
</Fragment> </Fragment>
)} )}
@ -58,14 +59,14 @@ class Clients extends Component {
Clients.propTypes = { Clients.propTypes = {
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
dashboard: PropTypes.object.isRequired, dashboard: PropTypes.object.isRequired,
stats: PropTypes.object.isRequired,
clients: PropTypes.object.isRequired, clients: PropTypes.object.isRequired,
toggleClientModal: PropTypes.func.isRequired, toggleClientModal: PropTypes.func.isRequired,
deleteClient: PropTypes.func.isRequired, deleteClient: PropTypes.func.isRequired,
addClient: PropTypes.func.isRequired, addClient: PropTypes.func.isRequired,
updateClient: PropTypes.func.isRequired, updateClient: PropTypes.func.isRequired,
getClients: PropTypes.func.isRequired, getClients: PropTypes.func.isRequired,
getTopStats: PropTypes.func.isRequired, getStats: PropTypes.func.isRequired,
topStats: PropTypes.object,
}; };
export default withNamespaces()(Clients); export default withNamespaces()(Clients);

View File

@ -88,7 +88,7 @@ class Settings extends Component {
<div className="col-md-12"> <div className="col-md-12">
<StatsConfig <StatsConfig
interval={stats.interval} interval={stats.interval}
processing={stats.setConfigProcessing} processing={stats.processingSetConfig}
setStatsConfig={setStatsConfig} setStatsConfig={setStatsConfig}
/> />
</div> </div>

View File

@ -4,33 +4,27 @@ import { ResponsiveLine } from '@nivo/line';
import './Line.css'; import './Line.css';
const Line = props => ( const Line = ({ data, color }) => (
props.data && data &&
<ResponsiveLine <ResponsiveLine
data={props.data} data={data}
margin={{ margin={{
top: 15, top: 15,
right: 0, right: 0,
bottom: 1, bottom: 1,
left: 0, left: 20,
}} }}
minY="auto" minY="auto"
stacked={false} stacked={false}
curve='linear' curve='linear'
axisBottom={{ axisBottom={null}
tickSize: 0, axisLeft={null}
tickPadding: 10,
}}
axisLeft={{
tickSize: 0,
tickPadding: 10,
}}
enableGridX={false} enableGridX={false}
enableGridY={false} enableGridY={false}
enableDots={false} enableDots={false}
enableArea={true} enableArea={true}
animate={false} animate={false}
colorBy={() => (props.color)} colorBy={() => (color)}
tooltip={slice => ( tooltip={slice => (
<div> <div>
{slice.data.map(d => ( {slice.data.map(d => (

View File

@ -1,20 +1,22 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getClients, getTopStats } from '../actions'; import { getClients } from '../actions';
import { getStats } from '../actions/stats';
import { addClient, updateClient, deleteClient, toggleClientModal } from '../actions/clients'; import { addClient, updateClient, deleteClient, toggleClientModal } from '../actions/clients';
import Clients from '../components/Settings/Clients'; import Clients from '../components/Settings/Clients';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { dashboard, clients } = state; const { dashboard, clients, stats } = state;
const props = { const props = {
dashboard, dashboard,
clients, clients,
stats,
}; };
return props; return props;
}; };
const mapDispatchToProps = { const mapDispatchToProps = {
getClients, getClients,
getTopStats, getStats,
addClient, addClient,
updateClient, updateClient,
deleteClient, deleteClient,

View File

@ -1,14 +1,23 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as actionCreators from '../actions'; import { toggleProtection, getClients } from '../actions';
import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats';
import Dashboard from '../components/Dashboard'; import Dashboard from '../components/Dashboard';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { dashboard } = state; const { dashboard, stats } = state;
const props = { dashboard }; const props = { dashboard, stats };
return props; return props;
}; };
const mapDispatchToProps = {
toggleProtection,
getClients,
getStats,
getStatsConfig,
setStatsConfig,
};
export default connect( export default connect(
mapStateToProps, mapStateToProps,
actionCreators, mapDispatchToProps,
)(Dashboard); )(Dashboard);

View File

@ -2,11 +2,12 @@ import dateParse from 'date-fns/parse';
import dateFormat from 'date-fns/format'; import dateFormat from 'date-fns/format';
import subHours from 'date-fns/sub_hours'; import subHours from 'date-fns/sub_hours';
import addHours from 'date-fns/add_hours'; import addHours from 'date-fns/add_hours';
import addDays from 'date-fns/add_days';
import subDays from 'date-fns/sub_days';
import round from 'lodash/round'; import round from 'lodash/round';
import axios from 'axios'; import axios from 'axios';
import { import {
STATS_NAMES,
STANDARD_DNS_PORT, STANDARD_DNS_PORT,
STANDARD_WEB_PORT, STANDARD_WEB_PORT,
STANDARD_HTTPS_PORT, STANDARD_HTTPS_PORT,
@ -49,29 +50,28 @@ export const normalizeLogs = logs => logs.map((log) => {
}; };
}); });
export const normalizeHistory = history => Object.keys(history).map((key) => { export const normalizeHistory = (history, interval) => {
let id = STATS_NAMES[key]; if (interval === 1 || interval === 7) {
if (!id) { const hoursAgo = subHours(Date.now(), 24 * interval);
id = key.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase()); return history.map((item, index) => ({
x: dateFormat(addHours(hoursAgo, index), 'D MMM HH:00'),
y: round(item, 2),
}));
} }
const dayAgo = subHours(Date.now(), 24); const daysAgo = subDays(Date.now(), interval - 1);
return history.map((item, index) => ({
x: dateFormat(addDays(daysAgo, index), 'D MMM YYYY'),
y: round(item, 2),
}));
};
const data = history[key].map((item, index) => { export const normalizeTopStats = stats => (
const formatHour = dateFormat(addHours(dayAgo, index), 'ddd HH:00'); stats.map(item => ({
const roundValue = round(item, 2); name: Object.keys(item)[0],
count: Object.values(item)[0],
return { }))
x: formatHour, );
y: roundValue,
};
});
return {
id,
data,
};
});
export const normalizeFilteringStatus = (filteringStatus) => { export const normalizeFilteringStatus = (filteringStatus) => {
const { enabled, filters, user_rules: userRules } = filteringStatus; const { enabled, filters, user_rules: userRules } = filteringStatus;

View File

@ -94,27 +94,6 @@ const dashboard = handleActions({
return newState; return newState;
}, },
[actions.getStatsRequest]: state => ({ ...state, processingStats: true }),
[actions.getStatsFailure]: state => ({ ...state, processingStats: false }),
[actions.getStatsSuccess]: (state, { payload }) => {
const newState = { ...state, stats: payload, processingStats: false };
return newState;
},
[actions.getTopStatsRequest]: state => ({ ...state, processingTopStats: true }),
[actions.getTopStatsFailure]: state => ({ ...state, processingTopStats: false }),
[actions.getTopStatsSuccess]: (state, { payload }) => {
const newState = { ...state, topStats: payload, processingTopStats: false };
return newState;
},
[actions.getStatsHistoryRequest]: state => ({ ...state, processingStatsHistory: true }),
[actions.getStatsHistoryFailure]: state => ({ ...state, processingStatsHistory: false }),
[actions.getStatsHistorySuccess]: (state, { payload }) => {
const newState = { ...state, statsHistory: payload, processingStatsHistory: false };
return newState;
},
[actions.toggleLogStatusRequest]: state => ({ ...state, logStatusProcessing: true }), [actions.toggleLogStatusRequest]: state => ({ ...state, logStatusProcessing: true }),
[actions.toggleLogStatusFailure]: state => ({ ...state, logStatusProcessing: false }), [actions.toggleLogStatusFailure]: state => ({ ...state, logStatusProcessing: false }),
[actions.toggleLogStatusSuccess]: (state) => { [actions.toggleLogStatusSuccess]: (state) => {
@ -200,8 +179,6 @@ const dashboard = handleActions({
}, { }, {
processing: true, processing: true,
isCoreRunning: false, isCoreRunning: false,
processingTopStats: true,
processingStats: true,
logStatusProcessing: false, logStatusProcessing: false,
processingVersion: true, processingVersion: true,
processingFiltering: true, processingFiltering: true,
@ -218,15 +195,6 @@ const dashboard = handleActions({
dnsVersion: '', dnsVersion: '',
clients: [], clients: [],
autoClients: [], autoClients: [],
topStats: [],
stats: {
dns_queries: '',
blocked_filtering: '',
replaced_safebrowsing: '',
replaced_parental: '',
replaced_safesearch: '',
avg_processing_time: '',
},
}); });
const queryLogs = handleActions({ const queryLogs = handleActions({

View File

@ -2,26 +2,83 @@ import { handleActions } from 'redux-actions';
import * as actions from '../actions/stats'; import * as actions from '../actions/stats';
const stats = handleActions({ const stats = handleActions(
[actions.getStatsConfigRequest]: state => ({ ...state, getConfigProcessing: true }), {
[actions.getStatsConfigFailure]: state => ({ ...state, getConfigProcessing: false }), [actions.getStatsConfigRequest]: state => ({ ...state, processingGetConfig: true }),
[actions.getStatsConfigSuccess]: (state, { payload }) => ({ [actions.getStatsConfigFailure]: state => ({ ...state, processingGetConfig: false }),
...state, [actions.getStatsConfigSuccess]: (state, { payload }) => ({
interval: payload.interval, ...state,
getConfigProcessing: false, interval: payload.interval,
}), processingGetConfig: false,
}),
[actions.setStatsConfigRequest]: state => ({ ...state, setConfigProcessing: true }), [actions.setStatsConfigRequest]: state => ({ ...state, processingSetConfig: true }),
[actions.setStatsConfigFailure]: state => ({ ...state, setConfigProcessing: false }), [actions.setStatsConfigFailure]: state => ({ ...state, processingSetConfig: false }),
[actions.setStatsConfigSuccess]: (state, { payload }) => ({ [actions.setStatsConfigSuccess]: (state, { payload }) => ({
...state, ...state,
interval: payload.interval, interval: payload.interval,
setConfigProcessing: false, processingSetConfig: false,
}), }),
}, {
getConfigProcessing: false, [actions.getStatsRequest]: state => ({ ...state, processingStats: true }),
setConfigProcessing: false, [actions.getStatsFailure]: state => ({ ...state, processingStats: false }),
interval: 1, [actions.getStatsSuccess]: (state, { payload }) => {
}); const {
dns_queries: dnsQueries,
blocked_filtering: blockedFiltering,
replaced_parental: replacedParental,
replaced_safebrowsing: replacedSafebrowsing,
top_blocked_domains: topBlockedDomains,
top_clients: topClients,
top_queried_domains: topQueriedDomains,
num_blocked_filtering: numBlockedFiltering,
num_dns_queries: numDnsQueries,
num_replaced_parental: numReplacedParental,
num_replaced_safebrowsing: numReplacedSafebrowsing,
num_replaced_safesearch: numReplacedSafesearch,
avg_processing_time: avgProcessingTime,
} = payload;
const newState = {
...state,
processingStats: false,
dnsQueries,
blockedFiltering,
replacedParental,
replacedSafebrowsing,
topBlockedDomains,
topClients,
topQueriedDomains,
numBlockedFiltering,
numDnsQueries,
numReplacedParental,
numReplacedSafebrowsing,
numReplacedSafesearch,
avgProcessingTime,
};
return newState;
},
},
{
processingGetConfig: false,
processingSetConfig: false,
processingStats: true,
interval: 1,
dnsQueries: [],
blockedFiltering: [],
replacedParental: [],
replacedSafebrowsing: [],
topBlockedDomains: [],
topClients: [],
topQueriedDomains: [],
numBlockedFiltering: 0,
numDnsQueries: 0,
numReplacedParental: 0,
numReplacedSafebrowsing: 0,
numReplacedSafesearch: 0,
avgProcessingTime: 0,
},
);
export default stats; export default stats;