diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 03a7741c..09f94851 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -361,5 +361,10 @@
"encryption_certificates_source_path": "Set a certificates file path",
"encryption_certificates_source_content":"Paste the certificates contents",
"encryption_key_source_path": "Set a private key file",
- "encryption_key_source_content": "Paste the private key contents"
+ "encryption_key_source_content": "Paste the private key contents",
+ "stats_params": "Statistics configuration",
+ "config_successfully_saved": "Configuration successfully saved",
+ "interval_24_hour": "24 hours",
+ "interval_days": "{{value}} days",
+ "time_period": "Time period"
}
diff --git a/client/src/actions/stats.js b/client/src/actions/stats.js
new file mode 100644
index 00000000..19175817
--- /dev/null
+++ b/client/src/actions/stats.js
@@ -0,0 +1,36 @@
+import { createAction } from 'redux-actions';
+import Api from '../api/Api';
+import { addErrorToast, addSuccessToast } from './index';
+
+const apiClient = new Api();
+
+export const getStatsConfigRequest = createAction('GET_LOGS_CONFIG_REQUEST');
+export const getStatsConfigFailure = createAction('GET_LOGS_CONFIG_FAILURE');
+export const getStatsConfigSuccess = createAction('GET_LOGS_CONFIG_SUCCESS');
+
+export const getStatsConfig = () => async (dispatch) => {
+ dispatch(getStatsConfigRequest());
+ try {
+ const data = await apiClient.getStatsInfo();
+ dispatch(getStatsConfigSuccess(data));
+ } catch (error) {
+ dispatch(addErrorToast({ error }));
+ dispatch(getStatsConfigFailure());
+ }
+};
+
+export const setStatsConfigRequest = createAction('SET_STATS_CONFIG_REQUEST');
+export const setStatsConfigFailure = createAction('SET_STATS_CONFIG_FAILURE');
+export const setStatsConfigSuccess = createAction('SET_STATS_CONFIG_SUCCESS');
+
+export const setStatsConfig = config => async (dispatch) => {
+ dispatch(setStatsConfigRequest());
+ try {
+ await apiClient.setStatsConfig(config);
+ dispatch(addSuccessToast('config_successfully_saved'));
+ dispatch(setStatsConfigSuccess(config));
+ } catch (error) {
+ dispatch(addErrorToast({ error }));
+ dispatch(setStatsConfigFailure());
+ }
+};
diff --git a/client/src/api/Api.js b/client/src/api/Api.js
index a857766c..2967ec53 100644
--- a/client/src/api/Api.js
+++ b/client/src/api/Api.js
@@ -527,4 +527,22 @@ export default class Api {
};
return this.makeRequest(path, method, parameters);
}
+
+ // Settings for statistics
+ STATS_INFO = { path: 'stats_info', method: 'GET' };
+ STATS_CONFIG = { path: 'stats_config', method: 'POST' };
+
+ getStatsInfo() {
+ const { path, method } = this.STATS_INFO;
+ return this.makeRequest(path, method);
+ }
+
+ setStatsConfig(data) {
+ const { path, method } = this.STATS_CONFIG;
+ const config = {
+ data,
+ headers: { 'Content-Type': 'application/json' },
+ };
+ return this.makeRequest(path, method, config);
+ }
}
diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css
index 7f12dbbe..0cc18f6e 100644
--- a/client/src/components/Settings/Settings.css
+++ b/client/src/components/Settings/Settings.css
@@ -104,3 +104,8 @@
min-width: 23px;
padding: 5px;
}
+
+.custom-control-label,
+.custom-control-label:before {
+ transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color;
+}
diff --git a/client/src/components/Settings/StatsConfig/Form.js b/client/src/components/Settings/StatsConfig/Form.js
new file mode 100644
index 00000000..f1c3df24
--- /dev/null
+++ b/client/src/components/Settings/StatsConfig/Form.js
@@ -0,0 +1,71 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Field, reduxForm } from 'redux-form';
+import { Trans, withNamespaces } from 'react-i18next';
+import flow from 'lodash/flow';
+
+import { renderRadioField, toNumber } from '../../../helpers/form';
+import { STATS_INTERVALS } from '../../../helpers/constants';
+
+const getIntervalFields = (processing, t, handleChange, toNumber) =>
+ STATS_INTERVALS.map((interval) => {
+ const title = interval === 1
+ ? t('interval_24_hour')
+ : t('interval_days', { value: interval });
+
+ return (
+
+ );
+ });
+
+const Form = (props) => {
+ const {
+ handleSubmit, handleChange, processing, t,
+ } = props;
+
+ return (
+
+ );
+};
+
+Form.propTypes = {
+ handleSubmit: PropTypes.func.isRequired,
+ handleChange: PropTypes.func,
+ change: PropTypes.func.isRequired,
+ submitting: PropTypes.bool.isRequired,
+ invalid: PropTypes.bool.isRequired,
+ processing: PropTypes.bool.isRequired,
+ t: PropTypes.func.isRequired,
+};
+
+export default flow([
+ withNamespaces(),
+ reduxForm({
+ form: 'logConfigForm',
+ }),
+])(Form);
diff --git a/client/src/components/Settings/StatsConfig/index.js b/client/src/components/Settings/StatsConfig/index.js
new file mode 100644
index 00000000..5513e7d6
--- /dev/null
+++ b/client/src/components/Settings/StatsConfig/index.js
@@ -0,0 +1,49 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withNamespaces } from 'react-i18next';
+import debounce from 'lodash/debounce';
+
+import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
+import Form from './Form';
+import Card from '../../ui/Card';
+
+class StatsConfig extends Component {
+ handleFormChange = debounce((values) => {
+ this.props.setStatsConfig(values);
+ }, DEBOUNCE_TIMEOUT);
+
+ render() {
+ const {
+ t,
+ interval,
+ processing,
+ } = this.props;
+
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+StatsConfig.propTypes = {
+ interval: PropTypes.number.isRequired,
+ processing: PropTypes.bool.isRequired,
+ setStatsConfig: PropTypes.func.isRequired,
+ t: PropTypes.func.isRequired,
+};
+
+export default withNamespaces()(StatsConfig);
diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js
index 7391cbaf..7d703cb8 100644
--- a/client/src/components/Settings/index.js
+++ b/client/src/components/Settings/index.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { withNamespaces, Trans } from 'react-i18next';
import Services from './Services';
+import StatsConfig from './StatsConfig';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle';
@@ -37,6 +38,7 @@ class Settings extends Component {
componentDidMount() {
this.props.initSettings(this.settings);
this.props.getBlockedServices();
+ this.props.getStatsConfig();
}
renderSettings = (settings) => {
@@ -62,7 +64,12 @@ class Settings extends Component {
render() {
const {
- settings, services, setBlockedServices, t,
+ settings,
+ services,
+ setBlockedServices,
+ setStatsConfig,
+ stats,
+ t,
} = this.props;
return (
@@ -78,6 +85,13 @@ class Settings extends Component {
+
+
+
{
- const { settings, services } = state;
+ const { settings, services, stats } = state;
const props = {
settings,
services,
+ stats,
};
return props;
};
@@ -17,6 +19,8 @@ const mapDispatchToProps = {
toggleSetting,
getBlockedServices,
setBlockedServices,
+ getStatsConfig,
+ setStatsConfig,
};
export default connect(
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index 74ac19c1..b2ec30c2 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -260,3 +260,5 @@ export const FILTERED_STATUS = {
FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService',
REWRITE: 'Rewrite',
};
+
+export const STATS_INTERVALS = [1, 7, 30, 90];
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index 2913f5cc..ef197cb8 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -11,6 +11,7 @@ import clients from './clients';
import access from './access';
import rewrites from './rewrites';
import services from './services';
+import stats from './stats';
const settings = handleActions({
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
@@ -218,6 +219,14 @@ const dashboard = handleActions({
clients: [],
autoClients: [],
topStats: [],
+ stats: {
+ dns_queries: '',
+ blocked_filtering: '',
+ replaced_safebrowsing: '',
+ replaced_parental: '',
+ replaced_safesearch: '',
+ avg_processing_time: '',
+ },
});
const queryLogs = handleActions({
@@ -230,7 +239,11 @@ const queryLogs = handleActions({
[actions.downloadQueryLogRequest]: state => ({ ...state, logsDownloading: true }),
[actions.downloadQueryLogFailure]: state => ({ ...state, logsDownloading: false }),
[actions.downloadQueryLogSuccess]: state => ({ ...state, logsDownloading: false }),
-}, { getLogsProcessing: false, logsDownloading: false });
+}, {
+ getLogsProcessing: false,
+ logsDownloading: false,
+ logs: [],
+});
const filtering = handleActions({
[actions.setRulesRequest]: state => ({ ...state, processingRules: true }),
@@ -426,6 +439,7 @@ export default combineReducers({
access,
rewrites,
services,
+ stats,
loadingBar: loadingBarReducer,
form: formReducer,
});
diff --git a/client/src/reducers/stats.js b/client/src/reducers/stats.js
new file mode 100644
index 00000000..48e07bb5
--- /dev/null
+++ b/client/src/reducers/stats.js
@@ -0,0 +1,27 @@
+import { handleActions } from 'redux-actions';
+
+import * as actions from '../actions/stats';
+
+const stats = handleActions({
+ [actions.getStatsConfigRequest]: state => ({ ...state, getConfigProcessing: true }),
+ [actions.getStatsConfigFailure]: state => ({ ...state, getConfigProcessing: false }),
+ [actions.getStatsConfigSuccess]: (state, { payload }) => ({
+ ...state,
+ interval: payload.interval,
+ getConfigProcessing: false,
+ }),
+
+ [actions.setStatsConfigRequest]: state => ({ ...state, setConfigProcessing: true }),
+ [actions.setStatsConfigFailure]: state => ({ ...state, setConfigProcessing: false }),
+ [actions.setStatsConfigSuccess]: (state, { payload }) => ({
+ ...state,
+ interval: payload.interval,
+ setConfigProcessing: false,
+ }),
+}, {
+ getConfigProcessing: false,
+ setConfigProcessing: false,
+ interval: 1,
+});
+
+export default stats;