From 8a8c7329f7d437031760e1f5719cc9f7ed3cc73a Mon Sep 17 00:00:00 2001
From: Ildar Kamalov <i.kamalov@adguard.com>
Date: Thu, 23 May 2019 14:14:22 +0300
Subject: [PATCH] + client: add runtime clients table

---
 client/src/__locales/en.json                  |   6 +-
 client/src/actions/index.js                   |  11 +-
 client/src/components/Dashboard/Clients.js    |   4 +-
 client/src/components/Dashboard/index.js      |   1 +
 client/src/components/Logs/index.js           |   3 +-
 .../Settings/Clients/AutoClients.js           | 131 ++++++++++++++++++
 client/src/components/Settings/index.js       |  37 +++--
 client/src/reducers/index.js                  |   4 +-
 8 files changed, 175 insertions(+), 22 deletions(-)
 create mode 100644 client/src/components/Settings/Clients/AutoClients.js

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 6ba40384..bbfe2336 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -286,7 +286,9 @@
     "client_deleted": "Client \"{{key}}\" successfully deleted",
     "client_added":  "Client \"{{key}}\" successfully added",
     "client_updated":  "Client \"{{key}}\" successfully updated",
-    "table_statistics": "Statistics (last 24 hours)",
+    "table_statistics": "Requests count (last 24 hours)",
     "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}}\"?",
+    "auto_clients_title": "Clients (runtime)",
+    "auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration"
 }
\ No newline at end of file
diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index 493a4140..39224388 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -213,9 +213,14 @@ export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
 export const getClients = () => async (dispatch) => {
     dispatch(getClientsRequest());
     try {
-        const clients = await apiClient.getClients();
-        const sortedClients = sortClients(clients);
-        dispatch(getClientsSuccess(sortedClients));
+        const data = await apiClient.getClients();
+        const sortedClients = data.clients && sortClients(data.clients);
+        const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients);
+
+        dispatch(getClientsSuccess({
+            clients: sortedClients || [],
+            autoClients: sortedAutoClients || [],
+        }));
     } catch (error) {
         dispatch(addErrorToast({ error }));
         dispatch(getClientsFailure());
diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js
index d7081e38..fbef279e 100644
--- a/client/src/components/Dashboard/Clients.js
+++ b/client/src/components/Dashboard/Clients.js
@@ -24,7 +24,8 @@ class Clients extends Component {
         Header: 'IP',
         accessor: 'ip',
         Cell: ({ value }) => {
-            const clientName = getClientName(this.props.clients, value);
+            const clientName = getClientName(this.props.clients, value)
+                || getClientName(this.props.autoClients, value);
             let client;
 
             if (clientName) {
@@ -79,6 +80,7 @@ Clients.propTypes = {
     dnsQueries: PropTypes.number.isRequired,
     refreshButton: PropTypes.node.isRequired,
     clients: PropTypes.array.isRequired,
+    autoClients: PropTypes.array.isRequired,
     t: PropTypes.func,
 };
 
diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js
index 84215d56..51ede02c 100644
--- a/client/src/components/Dashboard/index.js
+++ b/client/src/components/Dashboard/index.js
@@ -96,6 +96,7 @@ class Dashboard extends Component {
                                         refreshButton={refreshButton}
                                         topClients={dashboard.topStats.top_clients}
                                         clients={dashboard.clients}
+                                        autoClients={dashboard.autoClients}
                                     />
                                 </div>
                                 <div className="col-lg-6">
diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js
index 7791025a..beae9e53 100644
--- a/client/src/components/Logs/index.js
+++ b/client/src/components/Logs/index.js
@@ -196,7 +196,8 @@ class Logs extends Component {
             Cell: (row) => {
                 const { reason } = row.original;
                 const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
-                const clientName = getClientName(dashboard.clients, row.value);
+                const clientName = getClientName(dashboard.clients, row.value)
+                    || getClientName(dashboard.autoClients, row.value);
                 let client;
 
                 if (clientName) {
diff --git a/client/src/components/Settings/Clients/AutoClients.js b/client/src/components/Settings/Clients/AutoClients.js
new file mode 100644
index 00000000..1f750909
--- /dev/null
+++ b/client/src/components/Settings/Clients/AutoClients.js
@@ -0,0 +1,131 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withNamespaces } from 'react-i18next';
+import ReactTable from 'react-table';
+
+import { CLIENT_ID } from '../../../helpers/constants';
+import Card from '../../ui/Card';
+
+class AutoClients extends Component {
+    getClient = (name, clients) => {
+        const client = clients.find(item => name === item.name);
+
+        if (client) {
+            const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
+
+            return {
+                identifier,
+                use_global_settings: true,
+                ...client,
+            };
+        }
+
+        return {
+            identifier: 'ip',
+            use_global_settings: true,
+        };
+    };
+
+    getStats = (ip, stats) => {
+        if (stats && stats.top_clients) {
+            return stats.top_clients[ip];
+        }
+
+        return '';
+    };
+
+    columns = [
+        {
+            Header: this.props.t('table_client'),
+            accessor: 'ip',
+            Cell: (row) => {
+                if (row.value) {
+                    return (
+                        <div className="logs__row logs__row--overflow">
+                            <span className="logs__text" title={row.value}>
+                                {row.value} <em>(IP)</em>
+                            </span>
+                        </div>
+                    );
+                } else if (row.original && row.original.mac) {
+                    return (
+                        <div className="logs__row logs__row--overflow">
+                            <span className="logs__text" title={row.original.mac}>
+                                {row.original.mac} <em>(MAC)</em>
+                            </span>
+                        </div>
+                    );
+                }
+
+                return '';
+            },
+        },
+        {
+            Header: this.props.t('table_name'),
+            accessor: 'name',
+            Cell: ({ value }) => (
+                <div className="logs__row logs__row--overflow">
+                    <span className="logs__text" title={value}>
+                        {value}
+                    </span>
+                </div>
+            ),
+        },
+        {
+            Header: this.props.t('table_statistics'),
+            accessor: 'statistics',
+            Cell: (row) => {
+                const clientIP = row.original.ip;
+                const clientStats = clientIP && this.getStats(clientIP, this.props.topStats);
+
+                if (clientStats) {
+                    return (
+                        <div className="logs__row">
+                            <div className="logs__text" title={clientStats}>
+                                {clientStats}
+                            </div>
+                        </div>
+                    );
+                }
+
+                return '–';
+            },
+        },
+    ];
+
+    render() {
+        const { t, autoClients } = this.props;
+
+        return (
+            <Card
+                title={t('auto_clients_title')}
+                subtitle={t('auto_clients_desc')}
+                bodyType="card-body box-body--settings"
+            >
+                <ReactTable
+                    data={autoClients || []}
+                    columns={this.columns}
+                    className="-striped -highlight card-table-overflow"
+                    showPagination={true}
+                    defaultPageSize={10}
+                    minRows={5}
+                    previousText={t('previous_btn')}
+                    nextText={t('next_btn')}
+                    loadingText={t('loading_table_status')}
+                    pageText={t('page_table_footer_text')}
+                    ofText={t('of_table_footer_text')}
+                    rowsText={t('rows_table_footer_text')}
+                    noDataText={t('clients_not_found')}
+                />
+            </Card>
+        );
+    }
+}
+
+AutoClients.propTypes = {
+    t: PropTypes.func.isRequired,
+    autoClients: PropTypes.array.isRequired,
+    topStats: PropTypes.object.isRequired,
+};
+
+export default withNamespaces()(AutoClients);
diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js
index 9292efb3..3435e610 100644
--- a/client/src/components/Settings/index.js
+++ b/client/src/components/Settings/index.js
@@ -1,14 +1,17 @@
 import React, { Component, Fragment } from 'react';
 import PropTypes from 'prop-types';
 import { withNamespaces, Trans } from 'react-i18next';
+
 import Upstream from './Upstream';
 import Dhcp from './Dhcp';
 import Encryption from './Encryption';
 import Clients from './Clients';
+import AutoClients from './Clients/AutoClients';
 import Checkbox from '../ui/Checkbox';
 import Loading from '../ui/Loading';
 import PageTitle from '../ui/PageTitle';
 import Card from '../ui/Card';
+
 import './Settings.css';
 
 class Settings extends Component {
@@ -93,20 +96,26 @@ class Settings extends Component {
                                     processingSetUpstream={settings.processingSetUpstream}
                                 />
                                 {!dashboard.processingTopStats && !dashboard.processingClients && (
-                                    <Clients
-                                        clients={dashboard.clients}
-                                        topStats={dashboard.topStats}
-                                        isModalOpen={clients.isModalOpen}
-                                        modalClientName={clients.modalClientName}
-                                        modalType={clients.modalType}
-                                        addClient={this.props.addClient}
-                                        updateClient={this.props.updateClient}
-                                        deleteClient={this.props.deleteClient}
-                                        toggleClientModal={this.props.toggleClientModal}
-                                        processingAdding={clients.processingAdding}
-                                        processingDeleting={clients.processingDeleting}
-                                        processingUpdating={clients.processingUpdating}
-                                    />
+                                    <Fragment>
+                                        <Clients
+                                            clients={dashboard.clients}
+                                            topStats={dashboard.topStats}
+                                            isModalOpen={clients.isModalOpen}
+                                            modalClientName={clients.modalClientName}
+                                            modalType={clients.modalType}
+                                            addClient={this.props.addClient}
+                                            updateClient={this.props.updateClient}
+                                            deleteClient={this.props.deleteClient}
+                                            toggleClientModal={this.props.toggleClientModal}
+                                            processingAdding={clients.processingAdding}
+                                            processingDeleting={clients.processingDeleting}
+                                            processingUpdating={clients.processingUpdating}
+                                        />
+                                        <AutoClients
+                                            autoClients={dashboard.autoClients}
+                                            topStats={dashboard.topStats}
+                                        />
+                                    </Fragment>
                                 )}
                                 <Encryption
                                     encryption={this.props.encryption}
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index 206a0de4..0a3b8171 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -185,7 +185,8 @@ const dashboard = handleActions({
     [actions.getClientsSuccess]: (state, { payload }) => {
         const newState = {
             ...state,
-            clients: payload,
+            clients: payload.clients,
+            autoClients: payload.autoClients,
             processingClients: false,
         };
         return newState;
@@ -210,6 +211,7 @@ const dashboard = handleActions({
     dnsAddresses: [],
     dnsVersion: '',
     clients: [],
+    autoClients: [],
     topStats: [],
 });