From c40f7b4d5c62bd45fc2d80545d3a8b2f8366436d Mon Sep 17 00:00:00 2001
From: Ildar Kamalov <i.kamalov@adguard.com>
Date: Fri, 28 Sep 2018 16:30:52 +0300
Subject: [PATCH 1/2] Add "block" and "unblock" buttons to the Query Log

---
 client/src/components/Filters/UserRules.js |  2 +-
 client/src/components/Logs/Logs.css        | 35 +++++++++++
 client/src/components/Logs/index.js        | 70 +++++++++++++++++++++-
 client/src/containers/Logs.js              |  9 ++-
 4 files changed, 109 insertions(+), 7 deletions(-)

diff --git a/client/src/components/Filters/UserRules.js b/client/src/components/Filters/UserRules.js
index bfd87b89..3d4b4275 100644
--- a/client/src/components/Filters/UserRules.js
+++ b/client/src/components/Filters/UserRules.js
@@ -20,7 +20,7 @@ export default class UserRules extends Component {
                 subtitle="Enter one rule on a line. You can use either adblock rules or hosts files syntax."
             >
                 <form onSubmit={this.handleSubmit}>
-                    <textarea className="form-control" value={this.props.userRules} onChange={this.handleChange} />
+                    <textarea className="form-control form-control--textarea" value={this.props.userRules} onChange={this.handleChange} />
                     <div className="card-actions">
                         <button
                             className="btn btn-success btn-standart"
diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css
index be7b14a4..dd30d512 100644
--- a/client/src/components/Logs/Logs.css
+++ b/client/src/components/Logs/Logs.css
@@ -1,6 +1,8 @@
 .logs__row {
+    position: relative;
     display: flex;
     align-items: center;
+    min-height: 26px;
 }
 
 .logs__row--overflow {
@@ -24,3 +26,36 @@
     margin-left: 0;
     margin-right: 5px;
 }
+
+.logs__action {
+    position: absolute;
+    top: 10px;
+    right: 15px;
+    background-color: #fff;
+    border-radius: 4px;
+    transition: opacity 0.2s ease, visibility 0.2s ease;
+    visibility: hidden;
+    opacity: 0;
+}
+
+.logs__table .rt-td {
+    position: relative;
+}
+
+.logs__table .rt-tr:hover .logs__action {
+    visibility: visible;
+    opacity: 1;
+}
+
+.logs__table .rt-tr-group:first-child .tooltip-custom:before {
+    top: calc(100% + 12px);
+    bottom: initial;
+    z-index: 1;
+}
+
+.logs__table .rt-tr-group:first-child .tooltip-custom:after {
+    top: initial;
+    bottom: -4px;
+    border-top: 6px solid transparent;
+    border-bottom: 6px solid #585965;
+}
diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js
index b45d5e19..907f7de7 100644
--- a/client/src/components/Logs/index.js
+++ b/client/src/components/Logs/index.js
@@ -1,7 +1,9 @@
-import React, { Component } from 'react';
+import React, { Component, Fragment } from 'react';
 import PropTypes from 'prop-types';
 import ReactTable from 'react-table';
 import { saveAs } from 'file-saver/FileSaver';
+import escapeRegExp from 'lodash/escapeRegExp';
+import endsWith from 'lodash/endsWith';
 import PageTitle from '../ui/PageTitle';
 import Card from '../ui/Card';
 import Loading from '../ui/Loading';
@@ -13,6 +15,7 @@ const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt';
 class Logs extends Component {
     componentDidMount() {
         this.getLogs();
+        this.props.getFilteringStatus();
     }
 
     componentDidUpdate(prevProps) {
@@ -36,6 +39,48 @@ class Logs extends Component {
         return '';
     }
 
+    toggleBlocking = (type, domain) => {
+        const { userRules } = this.props.filtering;
+        const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
+        let blockingRule = `@@||${domain}^$important`;
+        let unblockingRule = `||${domain}^$important`;
+
+        if (type === 'unblock') {
+            blockingRule = `||${domain}^$important`;
+            unblockingRule = `@@||${domain}^$important`;
+        }
+
+        const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
+        const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
+
+        if (userRules.match(preparedBlockingRule)) {
+            this.props.setRules(userRules.replace(`${blockingRule}`, ''));
+            this.props.addSuccessToast(`Removing rule from custom list: ${blockingRule}`);
+        } else if (!userRules.match(preparedUnblockingRule)) {
+            this.props.setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
+            this.props.addSuccessToast(`Adding rule to custom list: ${unblockingRule}`);
+        }
+
+        this.props.getFilteringStatus();
+    }
+
+    renderBlockingButton(isFiltered, domain) {
+        const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
+        const buttonText = isFiltered ? 'Unblock' : 'Block';
+
+        return (
+            <div className="logs__action">
+                <button
+                    type="button"
+                    className={`btn btn-sm ${buttonClass}`}
+                    onClick={() => this.toggleBlocking(buttonText.toLowerCase(), domain)}
+                >
+                    {buttonText}
+                </button>
+            </div>
+        );
+    }
+
     renderLogs(logs) {
         const columns = [{
             Header: 'Time',
@@ -85,14 +130,14 @@ class Logs extends Component {
                         (<li key={index} title={response}>{response}</li>));
                     return (
                         <div className="logs__row">
-                            { this.renderTooltip(isFiltered, rule)}
+                            {this.renderTooltip(isFiltered, rule)}
                             <ul className="list-unstyled">{liNodes}</ul>
                         </div>
                     );
                 }
                 return (
                     <div className="logs__row">
-                        { this.renderTooltip(isFiltered, rule) }
+                        {this.renderTooltip(isFiltered, rule)}
                         <span>Empty</span>
                     </div>
                 );
@@ -101,11 +146,25 @@ class Logs extends Component {
             Header: 'Client',
             accessor: 'client',
             maxWidth: 250,
+            Cell: (row) => {
+                const { reason } = row.original;
+                const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
+
+                return (
+                    <Fragment>
+                        <div className="logs__row">
+                            {row.value}
+                        </div>
+                        {this.renderBlockingButton(isFiltered, row.original.domain)}
+                    </Fragment>
+                );
+            },
         },
         ];
 
         if (logs) {
             return (<ReactTable
+                className='logs__table'
                 data={logs}
                 columns={columns}
                 showPagination={false}
@@ -187,6 +246,11 @@ Logs.propTypes = {
     dashboard: PropTypes.object,
     toggleLogStatus: PropTypes.func,
     downloadQueryLog: PropTypes.func,
+    getFilteringStatus: PropTypes.func,
+    filtering: PropTypes.object,
+    userRules: PropTypes.string,
+    setRules: PropTypes.func,
+    addSuccessToast: PropTypes.func,
 };
 
 export default Logs;
diff --git a/client/src/containers/Logs.js b/client/src/containers/Logs.js
index c80632e5..13ca4258 100644
--- a/client/src/containers/Logs.js
+++ b/client/src/containers/Logs.js
@@ -1,10 +1,10 @@
 import { connect } from 'react-redux';
-import { getLogs, toggleLogStatus, downloadQueryLog } from '../actions';
+import { getLogs, toggleLogStatus, downloadQueryLog, getFilteringStatus, setRules, addSuccessToast } from '../actions';
 import Logs from '../components/Logs';
 
 const mapStateToProps = (state) => {
-    const { queryLogs, dashboard } = state;
-    const props = { queryLogs, dashboard };
+    const { queryLogs, dashboard, filtering } = state;
+    const props = { queryLogs, dashboard, filtering };
     return props;
 };
 
@@ -12,6 +12,9 @@ const mapDispatchToProps = {
     getLogs,
     toggleLogStatus,
     downloadQueryLog,
+    getFilteringStatus,
+    setRules,
+    addSuccessToast,
 };
 
 export default connect(

From e20bfe9d08d6c60c8f37ec49dcda2f446bdf0ce5 Mon Sep 17 00:00:00 2001
From: Ildar Kamalov <i.kamalov@adguard.com>
Date: Fri, 28 Sep 2018 17:47:34 +0300
Subject: [PATCH 2/2] Replace line endings on save

---
 client/src/actions/index.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index efb2db12..015aecd1 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -293,7 +293,10 @@ export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
 export const setRules = rules => async (dispatch) => {
     dispatch(setRulesRequest());
     try {
-        await apiClient.setRules(rules);
+        const replacedLineEndings = rules
+            .replace(/^\n/g, '')
+            .replace(/\n\s*\n/g, '\n');
+        await apiClient.setRules(replacedLineEndings);
         dispatch(addSuccessToast('Custom rules saved'));
         dispatch(setRulesSuccess());
     } catch (error) {