PLT-5709 Global error handler (#187)

* WIP Global error handler

Adding components
Adding actions

* Add general errors where errors are caught

* Filter out non displayable errors

* Initial setup to email errors

* Add current user/team to error email

* Fix a few lints

* Ensure errors are cleared when reporting a problem

* Add clear all and internationalization

* Fix trailing comma issue

* fix lints

* Use Formatted Text component

* Move error list out of channel

* Add mobile prefix to i18n id for connection error

* Set shouldRender to true in RightMenuDrawerItem.defaultProps

* Update component name for RightMenuDrawerItem

* Clean up propTypes in RightMenuDrawerItem

* Clean up styles in RightMenuDrawerItem

* Use selector to filter displayable errors

* Clarify code for constructing wrapper style in ErrorList

* Avoid indenting email body for error report

* Remove extraneous params from errors actions

* Rename action type for logging errors

* Rename action for logging errors

* Set shouldRender to true in RightMenuDrawerItem.defaultProps

* Make logged errors displayable by default

* Don't log error when websocket connection is closed

* Hide button to dismiss all errors if ShowErrorsList isn't enabled

* Display only last error in alert bar when ShowErrorsList is disabled

* Add error logging where missing in actions

* Always show button to report problem

* Only include errors in report problem email if any have been logged

* Clarify subject of report problem email

* Set ShowErrorsList to false in default config.json

* Remove unused component + translation for connection errors

* Capture entire error object not just message

* Display fallback if error.message is blank

* Log errors in login actions

* Fix construction of errors email body
This commit is contained in:
Chris Duarte 2017-03-12 10:42:02 -07:00 committed by enahum
parent f3dbb72f65
commit e17eb6c237
25 changed files with 599 additions and 97 deletions

View file

@ -3,8 +3,8 @@
import {AsyncStorage} from 'react-native';
import {batchActions} from 'redux-batched-actions';
import {ViewTypes} from 'app/constants';
import {logError, getLogErrorAction} from 'service/actions/errors';
import {ChannelTypes, GeneralTypes, TeamsTypes, UsersTypes} from 'service/constants';
export function loadStorage() {
@ -49,7 +49,13 @@ export function loadStorage() {
dispatch(batchActions(actions), getState);
} catch (error) {
// Error loading data
dispatch({type: GeneralTypes.REMOVED_APP_CREDENTIALS, error}, getState);
dispatch(batchActions([
{
type: ChannelTypes.REMOVED_APP_CREDENTIALS,
error
},
getLogErrorAction(error)
]), getState);
}
};
}
@ -90,7 +96,7 @@ export async function updateStorage(key, data) {
return mergedStorageData;
} catch (error) {
// TODO: Need to handle this error
logError(error);
return null;
}
}
@ -114,7 +120,7 @@ export function removeStorage() {
await AsyncStorage.removeItem('storage');
}
} catch (error) {
// TODO: Error removing data
logError(error);
}
dispatch({type: UsersTypes.RESET_LOGOUT_STATE}, getState);
};

View file

@ -0,0 +1,127 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {
Dimensions,
StyleSheet,
View,
TouchableOpacity
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import GeneralError from './general_error';
import Config from 'assets/config.json';
const {width: deviceWidth} = Dimensions.get('window');
const style = StyleSheet.create({
closeButton: {
height: 20,
width: 20,
borderRadius: 10,
marginBottom: 5,
borderColor: '#fff',
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center'
},
closeButtonContainer: {
alignItems: 'center',
justifyContent: 'center',
marginTop: 5
},
closeButtonText: {
color: '#fff'
},
container: {
paddingTop: 15,
paddingBottom: 15,
alignItems: 'center',
justifyContent: 'center',
minHeight: 75
},
wrapper: {
position: 'absolute',
top: 0,
left: 0,
width: deviceWidth,
overflow: 'hidden',
backgroundColor: 'rgba(255, 116, 92, 1)',
zIndex: 99999
}
});
export default class ErrorList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
dismissError: PropTypes.func.isRequired,
clearErrors: PropTypes.func.isRequired
}).isRequired,
errors: PropTypes.array.isRequired
}
renderErrorsList() {
const {errors} = this.props;
if (Config.ShowErrorsList) {
return errors.map((error, index) => (
<GeneralError
key={index}
dismiss={() => this.props.actions.dismissError(index)}
error={error.error}
/>
));
}
const lastErrorIndex = errors.length - 1;
const lastError = errors[lastErrorIndex];
if (lastError) {
return (
<GeneralError
dismiss={() => this.props.actions.dismissError(lastErrorIndex)}
error={lastError.error}
/>
);
}
return null;
}
renderDismissAll() {
if (this.props.errors.length <= 1 || !Config.ShowErrorsList) {
return null;
}
return (
<TouchableOpacity
style={style.closeButtonContainer}
onPress={() => this.props.actions.clearErrors()}
>
<View style={style.closeButton}>
<Icon
name='close'
size={10}
color='#fff'
/>
</View>
<FormattedText
id='mobile.components.error_list.dismiss_all'
defaultMessage='Dismiss All'
style={style.closeButtonText}
/>
</TouchableOpacity>
);
}
render() {
const wrapperStyle = [style.wrapper];
if (!this.props.errors.length) {
wrapperStyle.push({height: 0});
}
return (
<View style={wrapperStyle}>
<View style={style.container}>
{this.renderErrorsList()}
{this.renderDismissAll()}
</View>
</View>
);
}
}

View file

@ -0,0 +1,27 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getDisplayableErrors} from 'service/selectors/errors';
import {dismissError, clearErrors} from 'service/actions/errors';
import ErrorList from './error_list';
function mapStateToProps(state) {
return {
errors: getDisplayableErrors(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissError,
clearErrors
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ErrorList);

View file

@ -0,0 +1,71 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import {
StyleSheet,
View,
Text,
TouchableOpacity
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
const style = StyleSheet.create({
buttonContainer: {
width: 25,
height: 25,
alignItems: 'center',
justifyContent: 'center'
},
buttons: {
marginHorizontal: 15
},
container: {
alignSelf: 'stretch',
paddingHorizontal: 15,
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center'
},
message: {
flex: 1,
color: '#fff'
}
});
function GeneralError(props) {
const {error, dismiss} = props;
let message = error.message;
if (!message) {
if (error instanceof Error) {
message = error.toString();
} else {
message = 'An error occurred.';
}
}
return (
<View style={style.container}>
<Text style={style.message}>
{error.message}
</Text>
<TouchableOpacity
style={style.buttonContainer}
onPress={dismiss}
>
<Icon
name='close'
size={20}
color='#fff'
/>
</TouchableOpacity>
</View>
);
}
GeneralError.propTypes = {
dismiss: PropTypes.func.isRequired,
error: PropTypes.object.isRequired
};
export default GeneralError;

View file

@ -0,0 +1,6 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ErrorListContainer from './error_list_container';
export default ErrorListContainer;

View file

@ -16,7 +16,7 @@ import Drawer from 'app/components/drawer';
import FormattedText from 'app/components/formatted_text';
import {RouteTransitions} from 'app/navigation/routes';
import {getTheme} from 'service/selectors/entities/preferences';
import ErrorList from 'app/components/error_list';
import NavigationModal from './navigation_modal';
const navigationPanResponder = NavigationExperimental.Card.CardStackPanResponder;
@ -128,6 +128,7 @@ class Router extends React.Component {
return (
<View style={{flex: 1, flexDirection: 'column-reverse'}}>
<View style={{flex: 1}}>
<ErrorList/>
{renderedScenes}
</View>
{navBar}

View file

@ -6,7 +6,8 @@ import {
ScrollView,
StyleSheet,
Platform,
View
View,
Linking
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
@ -17,14 +18,43 @@ import RightMenuDrawerItem from './right_menu_drawer_item';
export default class RightMenuDrawer extends React.Component {
static propTypes = {
errors: React.PropTypes.array.isRequired,
currentUserId: React.PropTypes.string.isRequired,
currentTeamId: React.PropTypes.string.isRequired,
actions: React.PropTypes.shape({
goToModalAccountSettings: React.PropTypes.func.isRequired,
goToModalSelectTeam: React.PropTypes.func.isRequired,
clearErrors: React.PropTypes.func.isRequired,
logout: React.PropTypes.func.isRequired
}).isRequired,
theme: React.PropTypes.object
};
openErrorEmail = () => {
const recipient = 'feedback@mattermost.com';
const subject = 'Problem with Mattermost React Native app';
Linking.openURL(
`mailto:${recipient}?subject=${subject}&body=${this.errorEmailBody()}`
);
this.props.actions.clearErrors();
};
errorEmailBody = () => {
const {currentUserId, currentTeamId, errors} = this.props;
let contents = [
`Current User Id: ${currentUserId}`,
`Current Team Id: ${currentTeamId}`
];
if (errors.length) {
contents = contents.concat([
'',
'Errors:',
JSON.stringify(errors.map((e) => e.error))
]);
}
return contents.join('\n');
};
render() {
const Styles = getStyleSheet(this.props.theme);
@ -66,7 +96,7 @@ export default class RightMenuDrawer extends React.Component {
defaultMessage='Help'
/>
</RightMenuDrawerItem>
<RightMenuDrawerItem>
<RightMenuDrawerItem onPress={this.openErrorEmail}>
<Icon
style={Styles.icon}
name='phone'

View file

@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToModalAccountSettings, goBack, goToModalSelectTeam} from 'app/actions/navigation';
import {clearErrors} from 'service/actions/errors';
import {logout} from 'service/actions/users';
import {getTheme} from 'service/selectors/entities/preferences';
@ -14,7 +15,10 @@ import RightMenuDrawer from './right_menu_drawer';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
theme: getTheme(state),
errors: state.errors,
currentUserId: state.entities.users.currentId,
currentTeamId: state.entities.teams.currentId
};
}
@ -24,6 +28,7 @@ function mapDispatchToProps(dispatch) {
goToModalAccountSettings,
goBack,
goToModalSelectTeam,
clearErrors,
logout
}, dispatch)
};

View file

@ -1,40 +1,36 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import React, {Component, PropTypes} from 'react';
import {StyleSheet, TouchableHighlight, View} from 'react-native';
const Styles = StyleSheet.create({
const styles = StyleSheet.create({
item: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
height: 40,
paddingLeft: 10,
paddingRight: 10,
flex: 1,
flexDirection: 'row'
paddingHorizontal: 10
}
});
export default class MainMenuItem extends React.Component {
export default class RightMenuDrawerItem extends Component {
static propTypes = {
children: React.PropTypes.node,
onPress: React.PropTypes.func,
style: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
])
children: PropTypes.node,
onPress: PropTypes.func,
style: View.propTypes.style
}
render() {
const {onPress, style, children} = this.props;
return (
<TouchableHighlight
underlayColor='rgba(255, 255, 255, 0.3)'
onPress={this.props.onPress}
style={this.props.style}
onPress={onPress}
style={style}
>
<View style={Styles.item}>
{this.props.children}
<View style={styles.item}>
{children}
</View>
</TouchableHighlight>
);

View file

@ -2,5 +2,6 @@
"DefaultServerUrl": "http://localhost:8065",
"TestServerUrl": "http://localhost:8065",
"DefaultLocale": "en",
"DefaultTheme": "default"
"DefaultTheme": "default",
"ShowErrorsList": false
}

View file

@ -1502,6 +1502,7 @@
"mobile.channel_list.privateChannel": "Private Channel",
"mobile.channel_list.publicChannel": "Public Channel",
"mobile.components.channels_list_view.yourChannels": "Your channels:",
"mobile.components.error_list.dismiss_all": "Dismiss All",
"mobile.components.select_server_view.enterServerUrl": "Enter Server URL",
"mobile.components.select_server_view.continue": "Continue",
"mobile.components.select_server_view.siteUrlPlaceholder": "https://mattermost.example.com",

View file

@ -8,16 +8,23 @@ import {
PreferencesTypes,
UsersTypes
} from 'service/constants';
import {forceLogoutIfNecessary} from './helpers';
import {batchActions} from 'redux-batched-actions';
import Client from 'service/client';
import {logError, getLogErrorAction} from './errors';
import {forceLogoutIfNecessary} from './helpers';
export function selectChannel(channelId) {
return async (dispatch, getState) => {
dispatch({
type: ChannelTypes.SELECT_CHANNEL,
data: channelId
}, getState);
try {
dispatch({
type: ChannelTypes.SELECT_CHANNEL,
data: channelId
}, getState);
} catch (error) {
logError(error)(dispatch);
}
};
}
@ -45,7 +52,8 @@ export function createChannel(channel, userId) {
{
type: ChannelTypes.CHANNEL_MEMBERS_FAILURE,
error
}
},
getLogErrorAction(error)
]), getState);
return null;
}
@ -96,14 +104,9 @@ export function createDirectChannel(teamId, userId, otherUserId) {
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch(batchActions([
{
type: ChannelTypes.CREATE_CHANNEL_FAILURE,
error
},
{
type: ChannelTypes.CHANNEL_MEMBERS_FAILURE,
error
}
{type: ChannelTypes.CREATE_CHANNEL_FAILURE, error},
{type: ChannelTypes.CHANNEL_MEMBERS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return null;
}
@ -150,7 +153,11 @@ export function updateChannel(channel) {
updated = await Client.updateChannel(channel);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -181,7 +188,11 @@ export function updateChannelNotifyProps(userId, teamId, channelId, props) {
notifyProps = await Client.updateChannelNotifyProps(teamId, data);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.NOTIFY_PROPS_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.NOTIFY_PROPS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -209,7 +220,10 @@ export function getChannel(teamId, channelId) {
data = await Client.getChannel(teamId, channelId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.CHANNELS_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.CHANNELS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -251,14 +265,9 @@ export function fetchMyChannelsAndMembers(teamId) {
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch(batchActions([
{
type: ChannelTypes.CHANNELS_FAILURE,
error
},
{
type: ChannelTypes.CHANNEL_MEMBERS_FAILURE,
error
}
{type: ChannelTypes.CHANNELS_FAILURE, error},
{type: ChannelTypes.CHANNEL_MEMBERS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -293,7 +302,10 @@ export function getMyChannelMembers(teamId) {
channelMembers = await channelMembersRequest;
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.CHANNEL_MEMBERS_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.CHANNEL_MEMBERS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -317,7 +329,10 @@ export function leaveChannel(teamId, channelId) {
await Client.leaveChannel(teamId, channelId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.LEAVE_CHANNEL_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.LEAVE_CHANNEL_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -346,7 +361,10 @@ export function joinChannel(userId, teamId, channelId, channelName) {
}
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.JOIN_CHANNEL_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.JOIN_CHANNEL_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -385,7 +403,10 @@ export function deleteChannel(teamId, channelId) {
await Client.deleteChannel(teamId, channelId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.DELETE_CHANNEL_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.DELETE_CHANNEL_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -430,7 +451,10 @@ export function viewChannel(teamId, channelId) {
await Client.viewChannel(teamId, channelId, prevChannelId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.UPDATE_LAST_VIEWED_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.UPDATE_LAST_VIEWED_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -447,7 +471,10 @@ export function getMoreChannels(teamId, offset, limit = Constants.CHANNELS_CHUNK
channels = await Client.getMoreChannels(teamId, offset, limit);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.MORE_CHANNELS_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.MORE_CHANNELS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return null;
}
@ -474,7 +501,10 @@ export function searchMoreChannels(teamId, term) {
channels = await Client.searchMoreChannels(teamId, term);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.MORE_CHANNELS_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.MORE_CHANNELS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -499,7 +529,10 @@ export function getChannelStats(teamId, channelId) {
stat = await Client.getChannelStats(teamId, channelId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.CHANNEL_STATS_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.CHANNEL_STATS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -523,7 +556,10 @@ export function addChannelMember(teamId, channelId, userId) {
await Client.addChannelMember(teamId, channelId, userId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.ADD_CHANNEL_MEMBER_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.ADD_CHANNEL_MEMBER_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -548,7 +584,10 @@ export function removeChannelMember(teamId, channelId, userId) {
await Client.removeChannelMember(teamId, channelId, userId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.REMOVE_CHANNEL_MEMBER_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.REMOVE_CHANNEL_MEMBER_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -668,7 +707,10 @@ export function autocompleteChannels(teamId, term) {
data = await Client.autocompleteChannels(teamId, term);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: ChannelTypes.AUTOCOMPLETE_CHANNELS_FAILURE, error}, getState);
dispatch(batchActions([
{type: ChannelTypes.AUTOCOMPLETE_CHANNELS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}

40
service/actions/errors.js Normal file
View file

@ -0,0 +1,40 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ErrorTypes} from 'service/constants';
export function dismissErrorObject(index) {
return {
type: ErrorTypes.DISMISS_ERROR,
index
};
}
export function dismissError(index) {
return async (dispatch) => {
dispatch(dismissErrorObject(index));
};
}
export function getLogErrorAction(error, displayable = true) {
return {
type: ErrorTypes.LOG_ERROR,
displayable,
error
};
}
export function logError(error, displayable = true) {
return async (dispatch) => {
// do something with the incoming error
// like sending it to analytics
dispatch(getLogErrorAction(error, displayable));
};
}
export function clearErrors() {
return async (dispatch) => {
dispatch({type: ErrorTypes.CLEAR_ERRORS});
};
}

View file

@ -5,6 +5,7 @@ import {batchActions} from 'redux-batched-actions';
import Client from 'service/client';
import {FilesTypes} from 'service/constants';
import {getLogErrorAction} from 'service/actions/errors';
import {forceLogoutIfNecessary} from './helpers';
export function getFilesForPost(teamId, channelId, postId) {
@ -16,7 +17,10 @@ export function getFilesForPost(teamId, channelId, postId) {
files = await Client.getFileInfosForPost(teamId, channelId, postId);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: FilesTypes.FETCH_FILES_FOR_POST_FAILURE, error}, getState);
dispatch(batchActions([
{type: FilesTypes.FETCH_FILES_FOR_POST_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}

View file

@ -1,10 +1,13 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
import Client from 'service/client';
import {bindClientFunc, FormattedError} from './helpers.js';
import {GeneralTypes} from 'service/constants';
import {getMyChannelMembers} from './channels';
import {getLogErrorAction} from 'service/actions/errors';
export function getPing() {
return async (dispatch, getState) => {
@ -19,11 +22,17 @@ export function getPing() {
data = await Client.getPing();
if (!data.version) {
// successful ping but not the right return data
dispatch({type: GeneralTypes.PING_FAILURE, error: pingError}, getState);
dispatch(batchActions([
{type: GeneralTypes.PING_FAILURE, error: pingError},
getLogErrorAction(pingError)
]), getState);
return;
}
} catch (error) {
dispatch({type: GeneralTypes.PING_FAILURE, error: pingError}, getState);
dispatch(batchActions([
{type: GeneralTypes.PING_FAILURE, error: pingError},
getLogErrorAction(error)
]), getState);
return;
}

View file

@ -1,8 +1,10 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
import Client from 'service/client';
import {UsersTypes} from 'service/constants';
import {getLogErrorAction} from './errors';
const HTTP_UNAUTHORIZED = 401;
export async function forceLogoutIfNecessary(err, dispatch) {
@ -50,7 +52,10 @@ export function bindClientFunc(clientFunc, request, success, failure, ...args) {
data = await clientFunc(...args);
} catch (err) {
forceLogoutIfNecessary(err, dispatch);
dispatch(requestFailure(failure, err), getState);
dispatch(batchActions([
requestFailure(failure, err),
getLogErrorAction(err)
]), getState);
return;
}

View file

@ -5,6 +5,7 @@ import Client from 'service/client';
import {batchActions} from 'redux-batched-actions';
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
import {Constants, PostsTypes} from 'service/constants';
import {getLogErrorAction} from './errors';
import {getProfilesByIds, getStatusesByIds} from './users';
async function getProfilesAndStatusesForPosts(list, dispatch, getState) {
@ -65,7 +66,10 @@ export function deletePost(teamId, post) {
await Client.deletePost(teamId, post.channel_id, post.id);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.DELETE_POST_FAILURE, error}, getState);
dispatch(batchActions([
{type: PostsTypes.DELETE_POST_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -100,7 +104,10 @@ export function getPost(teamId, channelId, postId) {
getProfilesAndStatusesForPosts(post, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POST_FAILURE, error}, getState);
dispatch(batchActions([
{type: PostsTypes.GET_POST_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -127,7 +134,10 @@ export function getPosts(teamId, channelId, offset = 0, limit = Constants.POST_C
getProfilesAndStatusesForPosts(posts, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POSTS_FAILURE, error}, getState);
dispatch(batchActions([
{type: PostsTypes.GET_POSTS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return null;
}
@ -156,7 +166,10 @@ export function getPostsSince(teamId, channelId, since) {
getProfilesAndStatusesForPosts(posts, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POSTS_SINCE_FAILURE, error}, getState);
dispatch(batchActions([
{type: PostsTypes.GET_POSTS_SINCE_FAILURE, error},
getLogErrorAction(error)
]), getState);
return null;
}
@ -185,7 +198,10 @@ export function getPostsBefore(teamId, channelId, postId, offset = 0, limit = Co
getProfilesAndStatusesForPosts(posts, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POSTS_BEFORE_FAILURE, error}, getState);
dispatch(batchActions([
{type: PostsTypes.GET_POSTS_BEFORE_FAILURE, error},
getLogErrorAction(error)
]), getState);
return null;
}
@ -214,7 +230,10 @@ export function getPostsAfter(teamId, channelId, postId, offset = 0, limit = Con
getProfilesAndStatusesForPosts(posts, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POSTS_AFTER_FAILURE, error}, getState);
dispatch(batchActions([
{type: PostsTypes.GET_POSTS_AFTER_FAILURE, error},
getLogErrorAction(error)
]), getState);
return null;
}

View file

@ -11,6 +11,7 @@ import {getPreferenceKey} from 'service/utils/preference_utils';
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
import {getLogErrorAction} from './errors';
export function getMyPreferences() {
return bindClientFunc(
Client.getMyPreferences,
@ -28,7 +29,10 @@ export function savePreferences(preferences) {
await Client.savePreferences(preferences);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PreferencesTypes.SAVE_PREFERENCES_FAILURE, error}, getState);
dispatch(batchActions([
{type: PreferencesTypes.SAVE_PREFERENCES_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -52,7 +56,10 @@ export function deletePreferences(preferences) {
await Client.deletePreferences(preferences);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PreferencesTypes.DELETE_PREFERENCES_FAILURE, error}, getState);
dispatch(batchActions([
{type: PreferencesTypes.DELETE_PREFERENCES_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}

View file

@ -4,6 +4,7 @@
import Client from 'service/client';
import {batchActions} from 'redux-batched-actions';
import {Constants, TeamsTypes} from 'service/constants';
import {getLogErrorAction} from './errors';
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
import {getProfilesByIds, getStatusesByIds} from './users';
@ -65,7 +66,10 @@ export function createTeam(userId, team) {
created = await Client.createTeam(team);
} catch (err) {
forceLogoutIfNecessary(err, dispatch);
dispatch({type: TeamsTypes.CREATE_TEAM_FAILURE, error: err}, getState);
dispatch(batchActions([
{type: TeamsTypes.CREATE_TEAM_FAILURE, error: err},
getLogErrorAction(err)
]), getState);
return;
}
@ -127,7 +131,10 @@ export function getTeamMember(teamId, userId) {
getProfilesAndStatusesForMembers([userId], dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: TeamsTypes.TEAM_MEMBERS_FAILURE, error}, getState);
dispatch(batchActions([
{type: TeamsTypes.TEAM_MEMBERS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -153,7 +160,10 @@ export function getTeamMembersByIds(teamId, userIds) {
getProfilesAndStatusesForMembers(userIds, dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: TeamsTypes.TEAM_MEMBERS_FAILURE}, getState);
dispatch(batchActions([
{type: TeamsTypes.TEAM_MEMBERS_FAILURE, error},
getLogErrorAction(error)
]), getState);
}
dispatch(batchActions([
@ -184,9 +194,12 @@ export function addUserToTeam(teamId, userId) {
try {
await Client.addUserToTeam(teamId, userId);
} catch (err) {
forceLogoutIfNecessary(err, dispatch);
dispatch({type: TeamsTypes.ADD_TEAM_MEMBER_FAILURE, error: err}, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch(batchActions([
{type: TeamsTypes.ADD_TEAM_MEMBER_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -213,9 +226,12 @@ export function removeUserFromTeam(teamId, userId) {
try {
await Client.removeUserFromTeam(teamId, userId);
} catch (err) {
forceLogoutIfNecessary(err, dispatch);
dispatch({type: TeamsTypes.REMOVE_TEAM_MEMBER_FAILURE, error: err}, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch(batchActions([
{type: TeamsTypes.REMOVE_TEAM_MEMBER_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}

View file

@ -5,6 +5,7 @@ import {batchActions} from 'redux-batched-actions';
import Client from 'service/client';
import {Constants, PreferencesTypes, UsersTypes, TeamsTypes} from 'service/constants';
import {fetchTeams} from 'service/actions/teams';
import {getLogErrorAction} from 'service/actions/errors';
import {bindClientFunc, forceLogoutIfNecessary, debounce} from './helpers';
export function checkMfa(loginId) {
@ -15,7 +16,10 @@ export function checkMfa(loginId) {
dispatch({type: UsersTypes.CHECK_MFA_SUCCESS}, getState);
return mfa;
} catch (error) {
dispatch({type: UsersTypes.CHECK_MFA_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.CHECK_MFA_FAILURE, error},
getLogErrorAction(error)
]), getState);
return null;
}
};
@ -36,7 +40,10 @@ export function login(loginId, password, mfaToken = '') {
teamMembers = await teamMembersRequest;
preferences = await preferencesRequest;
} catch (error) {
dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.LOGIN_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -44,7 +51,10 @@ export function login(loginId, password, mfaToken = '') {
await fetchTeams()(dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.LOGIN_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -67,7 +77,10 @@ export function login(loginId, password, mfaToken = '') {
]), getState);
}).
catch((error) => {
dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.LOGIN_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
});
};
@ -81,7 +94,10 @@ export function loadMe() {
user = await Client.getMe();
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.LOGIN_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -91,7 +107,10 @@ export function loadMe() {
preferences = await Client.getMyPreferences();
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PreferencesTypes.MY_PREFERENCES_FAILURE, error}, getState);
dispatch(batchActions([
{type: PreferencesTypes.MY_PREFERENCES_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -99,7 +118,10 @@ export function loadMe() {
await fetchTeams()(dispatch, getState);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: TeamsTypes.FETCH_TEAMS_FAILURE, error}, getState);
dispatch(batchActions([
{type: TeamsTypes.FETCH_TEAMS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -109,7 +131,10 @@ export function loadMe() {
teamMembers = await Client.getMyTeamMembers();
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: TeamsTypes.MY_TEAM_MEMBERS_FAILURE, error}, getState);
dispatch(batchActions([
{type: TeamsTypes.MY_TEAM_MEMBERS_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -157,7 +182,10 @@ export function getProfiles(offset, limit = Constants.PROFILE_CHUNK_SIZE) {
profiles = await Client.getProfiles(offset, limit);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: UsersTypes.PROFILES_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.PROFILES_FAILURE, error},
getLogErrorAction(error)
]), getState);
return null;
}
@ -194,7 +222,10 @@ export function getProfilesInTeam(teamId, offset, limit = Constants.PROFILE_CHUN
profiles = await Client.getProfilesInTeam(teamId, offset, limit);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: UsersTypes.PROFILES_IN_TEAM_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.PROFILES_IN_TEAM_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -224,7 +255,10 @@ export function getProfilesInChannel(teamId, channelId, offset, limit = Constant
profiles = await Client.getProfilesInChannel(teamId, channelId, offset, limit);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: UsersTypes.PROFILES_IN_CHANNEL_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.PROFILES_IN_CHANNEL_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -254,7 +288,10 @@ export function getProfilesNotInChannel(teamId, channelId, offset, limit = Const
profiles = await Client.getProfilesNotInChannel(teamId, channelId, offset, limit);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: UsersTypes.PROFILES_NOT_IN_CHANNEL_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.PROFILES_NOT_IN_CHANNEL_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}
@ -341,7 +378,10 @@ export function autocompleteUsersInChannel(teamId, channelId, term) {
data = await Client.autocompleteUsersInChannel(teamId, channelId, term);
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: UsersTypes.AUTOCOMPLETE_IN_CHANNEL_FAILURE, error}, getState);
dispatch(batchActions([
{type: UsersTypes.AUTOCOMPLETE_IN_CHANNEL_FAILURE, error},
getLogErrorAction(error)
]), getState);
return;
}

View file

@ -0,0 +1,12 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import keyMirror from 'service/utils/key_mirror';
const ErrorTypes = keyMirror({
DISMISS_ERROR: null,
LOG_ERROR: null,
CLEAR_ERRORS: null
});
export default ErrorTypes;

View file

@ -3,6 +3,7 @@
import Constants from './constants';
import ChannelTypes from './channels';
import ErrorTypes from './errors';
import GeneralTypes from './general';
import UsersTypes from './users';
import TeamsTypes from './teams';
@ -19,6 +20,7 @@ const Preferences = {
export {
Constants,
ErrorTypes,
GeneralTypes,
UsersTypes,
TeamsTypes,

View file

@ -0,0 +1,27 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ErrorTypes} from 'service/constants';
export default (state = [], action) => {
switch (action.type) {
case ErrorTypes.DISMISS_ERROR: {
const nextState = [...state];
nextState.splice(action.index, 1);
return nextState;
}
case ErrorTypes.LOG_ERROR: {
const nextState = [...state];
const {displayable, error} = action;
nextState.push({displayable, error});
return nextState;
}
case ErrorTypes.CLEAR_ERRORS: {
return [];
}
default:
return state;
}
};

View file

@ -2,9 +2,11 @@
// See License.txt for license information.
import entities from './entities';
import errors from './errors';
import requests from './requests';
export default {
entities,
errors,
requests
};

View file

@ -0,0 +1,6 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export function getDisplayableErrors(state) {
return state.errors.filter((error) => error.displayable);
}