Merge pull request #20 in DNS/adguard-dns from feature/315 to master

* commit 'ded02d112c0e5b6d9585ec5506f24746abffdff3':
  Add console error
  Fix timeout
  Handle settings errors
  Show toast on failed request
  Fix clear interval
  Add alert on failed requests
This commit is contained in:
Eugene Bujak 2018-09-17 01:44:48 +03:00
commit ae50a2f827
10 changed files with 295 additions and 77 deletions

View File

@ -3849,6 +3849,11 @@
} }
} }
}, },
"dom-helpers": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.3.1.tgz",
"integrity": "sha512-2Sm+JaYn74OiTM2wHvxJOo3roiq/h25Yi69Fqk269cNUwIXsCvATB6CRSFC9Am/20G2b28hGv/+7NiWydIrPvg=="
},
"dom-serializer": { "dom-serializer": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
@ -9048,6 +9053,11 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"nanoid": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.2.3.tgz",
"integrity": "sha512-BAnxAdaihzMoszwhqRy8FPOX+dijs7esUEUYTIQ1KsOSKmCVNYnitAMmBDFxYzA6VQYvuUKw7o2K1AcMBTGzIg=="
},
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -12948,6 +12958,17 @@
"prop-types": "^15.5.6" "prop-types": "^15.5.6"
} }
}, },
"react-transition-group": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.4.0.tgz",
"integrity": "sha512-Xv5d55NkJUxUzLCImGSanK8Cl/30sgpOEMGc5m86t8+kZwrPxPCPcFqyx83kkr+5Lz5gs6djuvE5By+gce+VjA==",
"requires": {
"dom-helpers": "^3.3.1",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
}
},
"read-cache": { "read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View File

@ -15,6 +15,7 @@
"date-fns": "^1.29.0", "date-fns": "^1.29.0",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"nanoid": "^1.2.3",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"react": "^16.4.0", "react": "^16.4.0",
"react-click-outside": "^3.0.1", "react-click-outside": "^3.0.1",
@ -23,6 +24,7 @@
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-table": "^6.8.6", "react-table": "^6.8.6",
"react-transition-group": "^2.4.0",
"redux": "^4.0.0", "redux": "^4.0.0",
"redux-actions": "^2.4.0", "redux-actions": "^2.4.0",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",

View File

@ -6,45 +6,64 @@ import Api from '../api/Api';
const apiClient = new Api(); const apiClient = new Api();
export const addErrorToast = createAction('ADD_ERROR_TOAST');
export const addSuccessToast = createAction('ADD_SUCCESS_TOAST');
export const removeToast = createAction('REMOVE_TOAST');
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE'); export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW'); export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
export const toggleSetting = (settingKey, status) => async (dispatch) => { export const toggleSetting = (settingKey, status) => async (dispatch) => {
switch (settingKey) { let successMessage = '';
case 'filtering': try {
if (status) { // TODO move setting keys to constants
await apiClient.disableFiltering(); switch (settingKey) {
} else { case 'filtering':
await apiClient.enableFiltering(); if (status) {
} successMessage = 'Disabled filtering';
dispatch(toggleSettingStatus({ settingKey })); await apiClient.disableFiltering();
break; } else {
case 'safebrowsing': successMessage = 'Enabled filtering';
if (status) { await apiClient.enableFiltering();
await apiClient.disableSafebrowsing(); }
} else { dispatch(toggleSettingStatus({ settingKey }));
await apiClient.enableSafebrowsing(); break;
} case 'safebrowsing':
dispatch(toggleSettingStatus({ settingKey })); if (status) {
break; successMessage = 'Disabled safebrowsing';
case 'parental': await apiClient.disableSafebrowsing();
if (status) { } else {
await apiClient.disableParentalControl(); successMessage = 'Enabled safebrowsing';
} else { await apiClient.enableSafebrowsing();
await apiClient.enableParentalControl(); }
} dispatch(toggleSettingStatus({ settingKey }));
dispatch(toggleSettingStatus({ settingKey })); break;
break; case 'parental':
case 'safesearch': if (status) {
if (status) { successMessage = 'Disabled parental control';
await apiClient.disableSafesearch(); await apiClient.disableParentalControl();
} else { } else {
await apiClient.enableSafesearch(); successMessage = 'Enabled parental control';
} await apiClient.enableParentalControl();
dispatch(toggleSettingStatus({ settingKey })); }
break; dispatch(toggleSettingStatus({ settingKey }));
default: break;
break; case 'safesearch':
if (status) {
successMessage = 'Disabled safe search';
await apiClient.disableSafesearch();
} else {
successMessage = 'Enabled safe search';
await apiClient.enableSafesearch();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
default:
break;
}
dispatch(addSuccessToast(successMessage));
} catch (error) {
dispatch(addErrorToast({ error }));
} }
}; };
@ -73,7 +92,7 @@ export const initSettings = settingsList => async (dispatch) => {
}; };
dispatch(initSettingsSuccess({ settingsList: newSettingsList })); dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure()); dispatch(initSettingsFailure());
} }
}; };
@ -88,7 +107,7 @@ export const getDnsStatus = () => async (dispatch) => {
const dnsStatus = await apiClient.getGlobalStatus(); const dnsStatus = await apiClient.getGlobalStatus();
dispatch(dnsStatusSuccess(dnsStatus)); dispatch(dnsStatusSuccess(dnsStatus));
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure()); dispatch(initSettingsFailure());
} }
}; };
@ -103,7 +122,7 @@ export const enableDns = () => async (dispatch) => {
await apiClient.startGlobalFiltering(); await apiClient.startGlobalFiltering();
dispatch(enableDnsSuccess()); dispatch(enableDnsSuccess());
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(enableDnsFailure()); dispatch(enableDnsFailure());
} }
}; };
@ -118,8 +137,8 @@ export const disableDns = () => async (dispatch) => {
await apiClient.stopGlobalFiltering(); await apiClient.stopGlobalFiltering();
dispatch(disableDnsSuccess()); dispatch(disableDnsSuccess());
} catch (error) { } catch (error) {
console.error(error); dispatch(disableDnsFailure(error));
dispatch(disableDnsFailure()); dispatch(addErrorToast({ error }));
} }
}; };
@ -139,7 +158,7 @@ export const getStats = () => async (dispatch) => {
dispatch(getStatsSuccess(processedStats)); dispatch(getStatsSuccess(processedStats));
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(getStatsFailure()); dispatch(getStatsFailure());
} }
}; };
@ -150,19 +169,19 @@ export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
export const getTopStats = () => async (dispatch, getState) => { export const getTopStats = () => async (dispatch, getState) => {
dispatch(getTopStatsRequest()); dispatch(getTopStatsRequest());
try { const timer = setInterval(async () => {
const state = getState(); const state = getState();
const timer = setInterval(async () => { if (state.dashboard.isCoreRunning) {
if (state.dashboard.isCoreRunning) { clearInterval(timer);
try {
const stats = await apiClient.getGlobalStatsTop(); const stats = await apiClient.getGlobalStatsTop();
dispatch(getTopStatsSuccess(stats)); dispatch(getTopStatsSuccess(stats));
clearInterval(timer); } catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getTopStatsFailure(error));
} }
}, 100); }
} catch (error) { }, 100);
console.error(error);
dispatch(getTopStatsFailure());
}
}; };
export const getLogsRequest = createAction('GET_LOGS_REQUEST'); export const getLogsRequest = createAction('GET_LOGS_REQUEST');
@ -171,19 +190,19 @@ export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
export const getLogs = () => async (dispatch, getState) => { export const getLogs = () => async (dispatch, getState) => {
dispatch(getLogsRequest()); dispatch(getLogsRequest());
try { const timer = setInterval(async () => {
const state = getState(); const state = getState();
const timer = setInterval(async () => { if (state.dashboard.isCoreRunning) {
if (state.dashboard.isCoreRunning) { clearInterval(timer);
try {
const logs = normalizeLogs(await apiClient.getQueryLog()); const logs = normalizeLogs(await apiClient.getQueryLog());
dispatch(getLogsSuccess(logs)); dispatch(getLogsSuccess(logs));
clearInterval(timer); } catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getLogsFailure(error));
} }
}, 100); }
} catch (error) { }, 100);
console.error(error);
dispatch(getLogsFailure());
}
}; };
export const toggleLogStatusRequest = createAction('TOGGLE_LOGS_REQUEST'); export const toggleLogStatusRequest = createAction('TOGGLE_LOGS_REQUEST');
@ -202,7 +221,7 @@ export const toggleLogStatus = queryLogEnabled => async (dispatch) => {
await toggleMethod(); await toggleMethod();
dispatch(toggleLogStatusSuccess()); dispatch(toggleLogStatusSuccess());
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(toggleLogStatusFailure()); dispatch(toggleLogStatusFailure());
} }
}; };
@ -217,7 +236,7 @@ export const setRules = rules => async (dispatch) => {
await apiClient.setRules(rules); await apiClient.setRules(rules);
dispatch(setRulesSuccess()); dispatch(setRulesSuccess());
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(setRulesFailure()); dispatch(setRulesFailure());
} }
}; };
@ -232,7 +251,7 @@ export const getFilteringStatus = () => async (dispatch) => {
const status = await apiClient.getFilteringStatus(); const status = await apiClient.getFilteringStatus();
dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) })); dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) }));
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(getFilteringStatusFailure()); dispatch(getFilteringStatusFailure());
} }
}; };
@ -258,7 +277,7 @@ export const toggleFilterStatus = url => async (dispatch, getState) => {
dispatch(toggleFilterSuccess(url)); dispatch(toggleFilterSuccess(url));
dispatch(getFilteringStatus()); dispatch(getFilteringStatus());
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(toggleFilterFailure()); dispatch(toggleFilterFailure());
} }
}; };
@ -274,7 +293,7 @@ export const refreshFilters = () => async (dispatch) => {
dispatch(refreshFiltersSuccess); dispatch(refreshFiltersSuccess);
dispatch(getFilteringStatus()); dispatch(getFilteringStatus());
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(refreshFiltersFailure()); dispatch(refreshFiltersFailure());
} }
}; };
@ -292,7 +311,7 @@ export const getStatsHistory = () => async (dispatch) => {
const normalizedHistory = normalizeHistory(statsHistory); const normalizedHistory = normalizeHistory(statsHistory);
dispatch(getStatsHistorySuccess(normalizedHistory)); dispatch(getStatsHistorySuccess(normalizedHistory));
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(getStatsHistoryFailure()); dispatch(getStatsHistoryFailure());
} }
}; };
@ -308,7 +327,7 @@ export const addFilter = url => async (dispatch) => {
dispatch(addFilterSuccess(url)); dispatch(addFilterSuccess(url));
dispatch(getFilteringStatus()); dispatch(getFilteringStatus());
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(addFilterFailure()); dispatch(addFilterFailure());
} }
}; };
@ -325,7 +344,7 @@ export const removeFilter = url => async (dispatch) => {
dispatch(removeFilterSuccess(url)); dispatch(removeFilterSuccess(url));
dispatch(getFilteringStatus()); dispatch(getFilteringStatus());
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(removeFilterFailure()); dispatch(removeFilterFailure());
} }
}; };
@ -344,7 +363,7 @@ export const downloadQueryLog = () => async (dispatch) => {
data = await apiClient.downloadQueryLog(); data = await apiClient.downloadQueryLog();
dispatch(downloadQueryLogSuccess()); dispatch(downloadQueryLogSuccess());
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(downloadQueryLogFailure()); dispatch(downloadQueryLogFailure());
} }
return data; return data;
@ -361,7 +380,7 @@ export const setUpstream = url => async (dispatch) => {
await apiClient.setUpstream(url); await apiClient.setUpstream(url);
dispatch(setUpstreamSuccess()); dispatch(setUpstreamSuccess());
} catch (error) { } catch (error) {
console.error(error); dispatch(addErrorToast({ error }));
dispatch(setUpstreamFailure()); dispatch(setUpstreamFailure());
} }
}; };

View File

@ -6,12 +6,17 @@ export default class Api {
baseUrl = 'control'; baseUrl = 'control';
async makeRequest(path, method = 'POST', config) { async makeRequest(path, method = 'POST', config) {
const response = await axios({ try {
url: `${this.baseUrl}/${path}`, const response = await axios({
method, url: `${this.baseUrl}/${path}`,
...config, method,
}); ...config,
return response.data; });
return response.data;
} catch (error) {
console.error(error);
throw new Error(`${this.baseUrl}/${path} | ${error.response.data} | ${error.response.status}`);
}
} }
// Global methods // Global methods

View File

@ -13,6 +13,7 @@ import Settings from '../../containers/Settings';
import Filters from '../../containers/Filters'; import Filters from '../../containers/Filters';
import Logs from '../../containers/Logs'; import Logs from '../../containers/Logs';
import Footer from '../ui/Footer'; import Footer from '../ui/Footer';
import Toasts from '../Toasts';
import Status from '../ui/Status'; import Status from '../ui/Status';
@ -49,6 +50,7 @@ class App extends Component {
} }
</div> </div>
<Footer /> <Footer />
<Toasts />
</Fragment> </Fragment>
</HashRouter> </HashRouter>
); );
@ -60,6 +62,7 @@ App.propTypes = {
enableDns: PropTypes.func, enableDns: PropTypes.func,
dashboard: PropTypes.object, dashboard: PropTypes.object,
isCoreRunning: PropTypes.bool, isCoreRunning: PropTypes.bool,
error: PropTypes.string,
}; };
export default App; export default App;

View File

@ -26,7 +26,6 @@ class Dashboard extends Component {
dashboard.processingStatsHistory || dashboard.processingStatsHistory ||
dashboard.processingTopStats; dashboard.processingTopStats;
const disableButton = <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={() => this.props.disableDns()}>Disable DNS</button>;
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.componentDidMount()}>Refresh statistics</button>; const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.componentDidMount()}>Refresh statistics</button>;
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.componentDidMount()}></button>; const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.componentDidMount()}></button>;
@ -34,7 +33,6 @@ class Dashboard extends Component {
<Fragment> <Fragment>
<PageTitle title="Dashboard"> <PageTitle title="Dashboard">
<div className="page-title__actions"> <div className="page-title__actions">
{disableButton}
{refreshFullButton} {refreshFullButton}
</div> </div>
</PageTitle> </PageTitle>

View File

@ -0,0 +1,60 @@
.toasts {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 10;
width: 345px;
}
.toast {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
padding: 16px;
font-weight: 600;
color: #ffffff;
border-radius: 4px;
background-color: rgba(236, 53, 53, 0.75);
}
.toast--success {
background-color: rgba(90, 173, 99, 0.75);
}
.toast:last-child {
margin-bottom: 0;
}
.toast__content {
flex: 1 1 auto;
margin: 0 12px 0 0;
text-overflow: ellipsis;
overflow: hidden;
}
.toast__dismiss {
display: block;
flex: 0 0 auto;
padding: 0;
background: transparent;
border: 0;
cursor: pointer;
}
.toast-enter {
opacity: 0.01;
}
.toast-enter-active {
opacity: 1;
transition: all 0.3s ease-out;
}
.toast-exit {
opacity: 1;
}
.toast-exit-active {
opacity: 0.01;
transition: all 0.3s ease-out;
}

View File

@ -0,0 +1,38 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Toast extends Component {
componentDidMount() {
const timeout = this.props.type === 'error' ? 30000 : 5000;
setTimeout(() => {
this.props.removeToast(this.props.id);
}, timeout);
}
shouldComponentUpdate() {
return false;
}
render() {
return (
<div className={`toast toast--${this.props.type}`}>
<p className="toast__content">
{this.props.message}
</p>
<button className="toast__dismiss" onClick={() => this.props.removeToast(this.props.id)}>
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
</button>
</div>
);
}
}
Toast.propTypes = {
id: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
removeToast: PropTypes.func.isRequired,
};
export default Toast;

View File

@ -0,0 +1,42 @@
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import * as actionCreators from '../../actions';
import Toast from './Toast';
import './Toast.css';
const Toasts = props => (
<TransitionGroup className="toasts">
{props.toasts.notices && props.toasts.notices.map((toast) => {
const { id } = toast;
return (
<CSSTransition
key={id}
timeout={500}
classNames="toast"
>
<Toast removeToast={props.removeToast} {...toast} />
</CSSTransition>
);
})}
</TransitionGroup>
);
Toasts.propTypes = {
toasts: PropTypes.object,
removeToast: PropTypes.func,
};
const mapStateToProps = (state) => {
const { toasts } = state;
const props = { toasts };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(Toasts);

View File

@ -1,5 +1,6 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
import nanoid from 'nanoid';
import * as actions from '../actions'; import * as actions from '../actions';
@ -172,9 +173,38 @@ const filtering = handleActions({
userRules: '', userRules: '',
}); });
const toasts = handleActions({
[actions.addErrorToast]: (state, { payload }) => {
const errorToast = {
id: nanoid(),
message: payload.error.toString(),
type: 'error',
};
const newState = { ...state, notices: [...state.notices, errorToast] };
return newState;
},
[actions.addSuccessToast]: (state, { payload }) => {
const successToast = {
id: nanoid(),
message: payload,
type: 'success',
};
const newState = { ...state, notices: [...state.notices, successToast] };
return newState;
},
[actions.removeToast]: (state, { payload }) => {
const filtered = state.notices.filter(notice => notice.id !== payload);
const newState = { ...state, notices: filtered };
return newState;
},
}, { notices: [] });
export default combineReducers({ export default combineReducers({
settings, settings,
dashboard, dashboard,
queryLogs, queryLogs,
filtering, filtering,
toasts,
}); });