From f8202a74bd55b97cdd95c33d8f8ed97412ba43b1 Mon Sep 17 00:00:00 2001
From: Ildar Kamalov <i.kamalov@adguard.com>
Date: Thu, 28 Nov 2019 17:59:55 +0300
Subject: [PATCH] + client: handle clients find

---
 client/src/__locales/en.json               |  3 +-
 client/src/actions/queryLogs.js            |  7 ++--
 client/src/actions/stats.js                |  8 +++--
 client/src/components/Dashboard/Clients.js | 13 ++++----
 client/src/components/Dashboard/index.js   |  2 --
 client/src/components/Logs/index.js        | 14 ++++----
 client/src/helpers/formatClientCell.js     | 37 ++++++++++++----------
 client/src/helpers/helpers.js              | 22 +++++++++++++
 8 files changed, 67 insertions(+), 39 deletions(-)

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 4e834147..9ff11424 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -299,10 +299,11 @@
     "client_edit": "Edit Client",
     "client_identifier": "Identifier",
     "ip_address": "IP address",
-    "client_identifier_desc": "Clients can be identified by the IP address, MAC address, CIDR. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>",
+    "client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>",
     "form_enter_ip": "Enter IP",
     "form_enter_mac": "Enter MAC",
     "form_enter_id": "Enter identifier",
+    "form_add_id": "Add identifier",
     "form_client_name": "Enter client name",
     "client_global_settings": "Use global settings",
     "client_deleted": "Client \"{{key}}\" successfully deleted",
diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js
index 35b4f7af..155f0a76 100644
--- a/client/src/actions/queryLogs.js
+++ b/client/src/actions/queryLogs.js
@@ -2,7 +2,7 @@ import { createAction } from 'redux-actions';
 
 import apiClient from '../api/Api';
 import { addErrorToast, addSuccessToast } from './index';
-import { normalizeLogs } from '../helpers/helpers';
+import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
 import { TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants';
 
 const getLogsWithParams = async (config) => {
@@ -10,9 +10,12 @@ const getLogsWithParams = async (config) => {
     const rawLogs = await apiClient.getQueryLog({ ...filter, older_than });
     const { data, oldest } = rawLogs;
     const logs = normalizeLogs(data);
+    const clientsParams = getParamsForClientsSearch(logs, 'client');
+    const clients = await apiClient.findClients(clientsParams);
+    const logsWithClientInfo = addClientInfo(logs, clients, 'client');
 
     return {
-        logs, oldest, older_than, filter, ...values,
+        logs: logsWithClientInfo, oldest, older_than, filter, ...values,
     };
 };
 
diff --git a/client/src/actions/stats.js b/client/src/actions/stats.js
index d8ab5bf5..25897aab 100644
--- a/client/src/actions/stats.js
+++ b/client/src/actions/stats.js
@@ -2,7 +2,7 @@ import { createAction } from 'redux-actions';
 
 import apiClient from '../api/Api';
 import { addErrorToast, addSuccessToast } from './index';
-import { normalizeTopStats, secondsToMilliseconds } from '../helpers/helpers';
+import { normalizeTopStats, secondsToMilliseconds, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
 
 export const getStatsConfigRequest = createAction('GET_STATS_CONFIG_REQUEST');
 export const getStatsConfigFailure = createAction('GET_STATS_CONFIG_FAILURE');
@@ -43,11 +43,15 @@ export const getStats = () => async (dispatch) => {
     dispatch(getStatsRequest());
     try {
         const stats = await apiClient.getStats();
+        const normalizedTopClients = normalizeTopStats(stats.top_clients);
+        const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name');
+        const clients = await apiClient.findClients(clientsParams);
+        const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name');
 
         const normalizedStats = {
             ...stats,
             top_blocked_domains: normalizeTopStats(stats.top_blocked_domains),
-            top_clients: normalizeTopStats(stats.top_clients),
+            top_clients: topClientsWithInfo,
             top_queried_domains: normalizeTopStats(stats.top_queried_domains),
             avg_processing_time: secondsToMilliseconds(stats.avg_processing_time),
         };
diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js
index ace7ed21..e83addcb 100644
--- a/client/src/components/Dashboard/Clients.js
+++ b/client/src/components/Dashboard/Clients.js
@@ -28,19 +28,17 @@ const countCell = dnsQueries =>
         return <Cell value={value} percent={percent} color={percentColor} />;
     };
 
-const clientCell = (clients, autoClients, t) =>
+const clientCell = t =>
     function cell(row) {
-        const { value } = row;
-
         return (
             <div className="logs__row logs__row--overflow logs__row--column">
-                {formatClientCell(value, clients, autoClients, t)}
+                {formatClientCell(row, t)}
             </div>
         );
     };
 
 const Clients = ({
-    t, refreshButton, topClients, subtitle, clients, autoClients, dnsQueries,
+    t, refreshButton, topClients, subtitle, dnsQueries,
 }) => (
     <Card
         title={t('top_clients')}
@@ -49,9 +47,10 @@ const Clients = ({
         refresh={refreshButton}
     >
         <ReactTable
-            data={topClients.map(({ name: ip, count }) => ({
+            data={topClients.map(({ name: ip, count, info }) => ({
                 ip,
                 count,
+                info,
             }))}
             columns={[
                 {
@@ -59,7 +58,7 @@ const Clients = ({
                     accessor: 'ip',
                     sortMethod: (a, b) =>
                         parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
-                    Cell: clientCell(clients, autoClients, t),
+                    Cell: clientCell(t),
                 },
                 {
                     Header: <Trans>requests_count</Trans>,
diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js
index 1960a793..b4889db6 100644
--- a/client/src/components/Dashboard/index.js
+++ b/client/src/components/Dashboard/index.js
@@ -20,7 +20,6 @@ class Dashboard extends Component {
     getAllStats = () => {
         this.props.getStats();
         this.props.getStatsConfig();
-        this.props.getClients();
     };
 
     getToggleFilteringButton = () => {
@@ -44,7 +43,6 @@ class Dashboard extends Component {
         const { dashboard, stats, t } = this.props;
         const dashboardProcessing =
             dashboard.processing ||
-            dashboard.processingClients ||
             stats.processingStats ||
             stats.processingGetConfig;
 
diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js
index 68b9cc61..3b52f4e4 100644
--- a/client/src/components/Logs/index.js
+++ b/client/src/components/Logs/index.js
@@ -31,7 +31,6 @@ class Logs extends Component {
         this.props.setLogsPage(TABLE_FIRST_PAGE);
         this.getLogs(...INITIAL_REQUEST_DATA);
         this.props.getFilteringStatus();
-        this.props.getClients();
         this.props.getLogsConfig();
     }
 
@@ -191,9 +190,9 @@ class Logs extends Component {
         );
     };
 
-    getClientCell = ({ original, value }) => {
-        const { dashboard, t } = this.props;
-        const { clients, autoClients } = dashboard;
+    getClientCell = (row) => {
+        const { original } = row;
+        const { t } = this.props;
         const { reason, domain } = original;
         const isFiltered = this.checkFiltered(reason);
         const isRewrite = this.checkRewrite(reason);
@@ -201,7 +200,7 @@ class Logs extends Component {
         return (
             <Fragment>
                 <div className="logs__row logs__row--overflow logs__row--column">
-                    {formatClientCell(value, clients, autoClients, t)}
+                    {formatClientCell(row, t)}
                 </div>
                 {isRewrite ? (
                     <div className="logs__action">
@@ -232,12 +231,11 @@ class Logs extends Component {
     };
 
     renderLogs() {
-        const { queryLogs, dashboard, t } = this.props;
-        const { processingClients } = dashboard;
+        const { queryLogs, t } = this.props;
         const {
             processingGetLogs, processingGetConfig, logs, pages, page,
         } = queryLogs;
-        const isLoading = processingGetLogs || processingClients || processingGetConfig;
+        const isLoading = processingGetLogs || processingGetConfig;
 
         const columns = [
             {
diff --git a/client/src/helpers/formatClientCell.js b/client/src/helpers/formatClientCell.js
index 30e9e99b..c5626061 100644
--- a/client/src/helpers/formatClientCell.js
+++ b/client/src/helpers/formatClientCell.js
@@ -1,5 +1,5 @@
 import React, { Fragment } from 'react';
-import { getClientInfo, getAutoClientInfo, normalizeWhois } from './helpers';
+import { normalizeWhois } from './helpers';
 import { WHOIS_ICONS } from './constants';
 
 const getFormattedWhois = (whois, t) => {
@@ -22,26 +22,29 @@ const getFormattedWhois = (whois, t) => {
     );
 };
 
-export const formatClientCell = (value, clients, autoClients, t) => {
-    const clientInfo = getClientInfo(clients, value) || getAutoClientInfo(autoClients, value);
-    const { name, whois } = clientInfo;
+export const formatClientCell = (row, t) => {
+    const { value, original: { info } } = row;
     let whoisContainer = '';
     let nameContainer = value;
 
-    if (name) {
-        nameContainer = (
-            <span className="logs__text logs__text--wrap" title={`${name} (${value})`}>
-                {name} <small>({value})</small>
-            </span>
-        );
-    }
+    if (info) {
+        const { name, whois } = info;
 
-    if (whois) {
-        whoisContainer = (
-            <div className="logs__text logs__text--wrap logs__text--whois">
-                {getFormattedWhois(whois, t)}
-            </div>
-        );
+        if (name) {
+            nameContainer = (
+                <span className="logs__text logs__text--wrap" title={`${name} (${value})`}>
+                    {name} <small>({value})</small>
+                </span>
+            );
+        }
+
+        if (whois) {
+            whoisContainer = (
+                <div className="logs__text logs__text--wrap logs__text--whois">
+                    {getFormattedWhois(whois, t)}
+                </div>
+            );
+        }
     }
 
     return (
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index 82389111..089f0604 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -8,6 +8,7 @@ import subDays from 'date-fns/sub_days';
 import round from 'lodash/round';
 import axios from 'axios';
 import i18n from 'i18next';
+import uniqBy from 'lodash/uniqBy';
 import versionCompare from './versionCompare';
 
 import {
@@ -92,6 +93,17 @@ export const normalizeTopStats = stats => (
     }))
 );
 
+export const addClientInfo = (data, clients, param) => (
+    data.map((row) => {
+        const clientIp = row[param];
+        const info = clients.find(item => item[clientIp]) || '';
+        return {
+            ...row,
+            info: (info && info[clientIp]) || '',
+        };
+    })
+);
+
 export const normalizeFilteringStatus = (filteringStatus) => {
     const {
         enabled, filters, user_rules: userRules, interval,
@@ -342,3 +354,13 @@ export const getPathWithQueryString = (path, params) => {
 
     return `${path}?${searchParams.toString()}`;
 };
+
+export const getParamsForClientsSearch = (data, param) => {
+    const uniqueClients = uniqBy(data, param);
+    return uniqueClients
+        .reduce((acc, item, idx) => {
+            const key = `ip${idx}`;
+            acc[key] = item[param];
+            return acc;
+        }, {});
+};