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) {