Show toast on failed request

This commit is contained in:
Ildar Kamalov
2018-09-14 15:37:35 +03:00
parent 9258fada47
commit 828bb40084
11 changed files with 230 additions and 32 deletions

View File

@@ -1,12 +1,15 @@
import { createAction } from 'redux-actions';
import round from 'lodash/round';
import alertify from 'alertifyjs';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers';
import Api from '../api/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 showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
@@ -75,6 +78,7 @@ export const initSettings = settingsList => async (dispatch) => {
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure());
}
};
@@ -90,6 +94,7 @@ export const getDnsStatus = () => async (dispatch) => {
dispatch(dnsStatusSuccess(dnsStatus));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure());
}
};
@@ -105,6 +110,7 @@ export const enableDns = () => async (dispatch) => {
dispatch(enableDnsSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(enableDnsFailure());
}
};
@@ -120,8 +126,8 @@ export const disableDns = () => async (dispatch) => {
dispatch(disableDnsSuccess());
} catch (error) {
console.error(error);
alertify.error(`Failed to disable DNS with status code ${error.response.status}`);
dispatch(disableDnsFailure());
dispatch(disableDnsFailure(error));
dispatch(addErrorToast({ error }));
}
};
@@ -142,6 +148,7 @@ export const getStats = () => async (dispatch) => {
dispatch(getStatsSuccess(processedStats));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getStatsFailure());
}
};
@@ -160,8 +167,9 @@ export const getTopStats = () => async (dispatch, getState) => {
const stats = await apiClient.getGlobalStatsTop();
dispatch(getTopStatsSuccess(stats));
} catch (error) {
alertify.error(`Failed to load statistics with status code ${error.response.status}`);
dispatch(getTopStatsFailure());
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getTopStatsFailure(error));
}
}
}, 100);
@@ -181,8 +189,9 @@ export const getLogs = () => async (dispatch, getState) => {
const logs = normalizeLogs(await apiClient.getQueryLog());
dispatch(getLogsSuccess(logs));
} catch (error) {
alertify.error(`Failed to load query log with status code ${error.response.status}`);
dispatch(getLogsFailure());
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getLogsFailure(error));
}
}
}, 100);
@@ -205,6 +214,7 @@ export const toggleLogStatus = queryLogEnabled => async (dispatch) => {
dispatch(toggleLogStatusSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(toggleLogStatusFailure());
}
};
@@ -220,6 +230,7 @@ export const setRules = rules => async (dispatch) => {
dispatch(setRulesSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(setRulesFailure());
}
};
@@ -235,6 +246,7 @@ export const getFilteringStatus = () => async (dispatch) => {
dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) }));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getFilteringStatusFailure());
}
};
@@ -261,6 +273,7 @@ export const toggleFilterStatus = url => async (dispatch, getState) => {
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(toggleFilterFailure());
}
};
@@ -277,6 +290,7 @@ export const refreshFilters = () => async (dispatch) => {
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(refreshFiltersFailure());
}
};
@@ -295,6 +309,7 @@ export const getStatsHistory = () => async (dispatch) => {
dispatch(getStatsHistorySuccess(normalizedHistory));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getStatsHistoryFailure());
}
};
@@ -311,6 +326,7 @@ export const addFilter = url => async (dispatch) => {
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(addFilterFailure());
}
};
@@ -328,6 +344,7 @@ export const removeFilter = url => async (dispatch) => {
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(removeFilterFailure());
}
};
@@ -347,6 +364,7 @@ export const downloadQueryLog = () => async (dispatch) => {
dispatch(downloadQueryLogSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(downloadQueryLogFailure());
}
return data;
@@ -364,6 +382,7 @@ export const setUpstream = url => async (dispatch) => {
dispatch(setUpstreamSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(setUpstreamFailure());
}
};

View File

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

View File

@@ -3,10 +3,8 @@ import { HashRouter, Route } from 'react-router-dom';
import PropTypes from 'prop-types';
import 'react-table/react-table.css';
import 'alertifyjs/build/css/alertify.min.css';
import '../ui/Tabler.css';
import '../ui/ReactTable.css';
import '../ui/Alertify.css';
import './index.css';
import Header from '../../containers/Header';
@@ -15,6 +13,7 @@ import Settings from '../../containers/Settings';
import Filters from '../../containers/Filters';
import Logs from '../../containers/Logs';
import Footer from '../ui/Footer';
import Toasts from '../Toasts';
import Status from '../ui/Status';
@@ -51,6 +50,7 @@ class App extends Component {
}
</div>
<Footer />
<Toasts />
</Fragment>
</HashRouter>
);
@@ -62,6 +62,7 @@ App.propTypes = {
enableDns: PropTypes.func,
dashboard: PropTypes.object,
isCoreRunning: PropTypes.bool,
error: PropTypes.string,
};
export default App;

View File

@@ -26,7 +26,6 @@ class Dashboard extends Component {
dashboard.processingStatsHistory ||
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 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>
<PageTitle title="Dashboard">
<div className="page-title__actions">
{disableButton}
{refreshFullButton}
</div>
</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,36 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Toast extends Component {
componentDidMount() {
setTimeout(() => {
this.props.removeToast(this.props.id);
}, 30000);
}
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,9 +0,0 @@
.alertify-notifier .ajs-message {
width: 280px;
font-weight: 600;
color: #fff;
}
.alertify-notifier .ajs-message.ajs-error {
background-color: rgba(236, 53, 53, 0.76);
}

View File

@@ -1,5 +1,6 @@
import { combineReducers } from 'redux';
import { handleActions } from 'redux-actions';
import nanoid from 'nanoid';
import * as actions from '../actions';
@@ -172,9 +173,38 @@ const filtering = handleActions({
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({
settings,
dashboard,
queryLogs,
filtering,
toasts,
});