From 011bc3e36bb5ede69f1e459e32004adc748e1f9d Mon Sep 17 00:00:00 2001
From: Ildar Kamalov <i.kamalov@adguard.com>
Date: Thu, 8 Aug 2019 13:43:06 +0300
Subject: [PATCH] + client: handle time interval for statistics

---
 client/src/__locales/en.json                  |  7 +-
 client/src/actions/stats.js                   | 36 ++++++++++
 client/src/api/Api.js                         | 18 +++++
 client/src/components/Settings/Settings.css   |  5 ++
 .../components/Settings/StatsConfig/Form.js   | 71 +++++++++++++++++++
 .../components/Settings/StatsConfig/index.js  | 49 +++++++++++++
 client/src/components/Settings/index.js       | 18 ++++-
 client/src/containers/Settings.js             |  6 +-
 client/src/helpers/constants.js               |  2 +
 client/src/reducers/index.js                  | 16 ++++-
 client/src/reducers/stats.js                  | 27 +++++++
 11 files changed, 251 insertions(+), 4 deletions(-)
 create mode 100644 client/src/actions/stats.js
 create mode 100644 client/src/components/Settings/StatsConfig/Form.js
 create mode 100644 client/src/components/Settings/StatsConfig/index.js
 create mode 100644 client/src/reducers/stats.js

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 (
+            <Field
+                key={interval}
+                name="interval"
+                type="radio"
+                component={renderRadioField}
+                value={interval}
+                placeholder={title}
+                onChange={handleChange}
+                normalize={toNumber}
+                disabled={processing}
+            />
+        );
+    });
+
+const Form = (props) => {
+    const {
+        handleSubmit, handleChange, processing, t,
+    } = props;
+
+    return (
+        <form onSubmit={handleSubmit}>
+            <div className="row">
+                <div className="col-12">
+                    <label className="form__label" htmlFor="server_name">
+                        <Trans>time_period</Trans>
+                    </label>
+                </div>
+                <div className="col-12">
+                    <div className="form__group">
+                        <div className="custom-controls-stacked">
+                            {getIntervalFields(processing, t, handleChange, toNumber)}
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </form>
+    );
+};
+
+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 (
+            <Card
+                title={t('stats_params')}
+                bodyType="card-body box-body--settings"
+            >
+                <div className="form">
+                    <Form
+                        initialValues={{
+                            interval,
+                        }}
+                        onSubmit={this.handleFormChange}
+                        onChange={this.handleFormChange}
+                        processing={processing}
+                    />
+                </div>
+            </Card>
+        );
+    }
+}
+
+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 (
             <Fragment>
@@ -78,6 +85,13 @@ class Settings extends Component {
                                     </div>
                                 </Card>
                             </div>
+                            <div className="col-md-12">
+                                <StatsConfig
+                                    interval={stats.interval}
+                                    processing={stats.setConfigProcessing}
+                                    setStatsConfig={setStatsConfig}
+                                />
+                            </div>
                             <div className="col-md-12">
                                 <Services
                                     services={services}
@@ -97,6 +111,8 @@ Settings.propTypes = {
     settings: PropTypes.object,
     settingsList: PropTypes.object,
     toggleSetting: PropTypes.func,
+    getStatsConfig: PropTypes.func,
+    setStatsConfig: PropTypes.func,
     t: PropTypes.func,
 };
 
diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js
index 054d1d1b..d13048ae 100644
--- a/client/src/containers/Settings.js
+++ b/client/src/containers/Settings.js
@@ -1,13 +1,15 @@
 import { connect } from 'react-redux';
 import { initSettings, toggleSetting } from '../actions';
 import { getBlockedServices, setBlockedServices } from '../actions/services';
+import { getStatsConfig, setStatsConfig } from '../actions/stats';
 import Settings from '../components/Settings';
 
 const mapStateToProps = (state) => {
-    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;